"""Deploy the official DeepSeek configuration without exposing credentials.""" from __future__ import annotations import json import posixpath import shlex import stat import time from datetime import datetime from pathlib import Path import paramiko ROOT = Path(__file__).resolve().parents[1] def load_config(path: Path) -> dict[str, str]: values: dict[str, str] = {} for raw in path.read_text(encoding="utf-8-sig").splitlines(): line = raw.strip() if line and not line.startswith("#") and "=" in line: key, value = line.split("=", 1) values[key.strip()] = value.strip() return values def connect(config: dict[str, str]) -> paramiko.SSHClient: client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) options = { "hostname": config["SERVER_HOST"], "port": int(config.get("SERVER_PORT", "22")), "username": config["SERVER_USER"], "timeout": 20, "banner_timeout": 20, "auth_timeout": 20, } if config.get("SERVER_KEYFILE"): options["key_filename"] = config["SERVER_KEYFILE"] else: options["password"] = config["SERVER_PASSWORD"] client.connect(**options) return client def run(client: paramiko.SSHClient, command: str, timeout: int = 300) -> str: _, stdout, stderr = client.exec_command(command, timeout=timeout) output = stdout.read().decode("utf-8", errors="replace") error = stderr.read().decode("utf-8", errors="replace") code = stdout.channel.recv_exit_status() if code: raise RuntimeError((error or output or f"remote exit {code}").strip()) return output.strip() def put_text(sftp: paramiko.SFTPClient, path: str, content: str, mode: int = 0o644) -> None: temporary = path + ".tmp" with sftp.file(temporary, "w") as handle: handle.write(content) handle.flush() sftp.chmod(temporary, mode) sftp.posix_rename(temporary, path) def update_env(content: str, updates: dict[str, str]) -> str: remaining = dict(updates) result: list[str] = [] for line in content.splitlines(): if "=" in line and not line.lstrip().startswith("#"): key = line.split("=", 1)[0].strip() if key in remaining: result.append(f"{key}={remaining.pop(key)}") continue result.append(line) result.extend(f"{key}={value}" for key, value in remaining.items()) return "\n".join(result) + "\n" def main() -> int: config = load_config(ROOT / "server-deploy.conf") secret = config.get("DEEPSEEK_API_KEY", "") if not secret.startswith("sk-") or len(secret) < 20: raise RuntimeError("DEEPSEEK_API_KEY is missing from server-deploy.conf") base = config["SERVER_DEPLOY_PATH"].rstrip("/") port = config.get("PORT", "3200") stamp = datetime.now().strftime("%Y%m%d%H%M%S") client = connect(config) sftp = client.open_sftp() try: current_image = run(client, "docker inspect -f '{{.Config.Image}}' mini-openclaw") if not current_image.startswith("mini-openclaw:"): raise RuntimeError("Unexpected current image; deployment was not changed") new_image = f"mini-openclaw:deepseek-official-{stamp}" rollback_container = f"mini-openclaw-rollback-{stamp}" local_files = ( "backend/services/assistant_model.py", "backend/services/project_assistant.py", "backend/api/risk_agent_config.py", "backend/test_assistant_components.py", "backend/test_project_assistant.py", "backend/test_risk_agent_config.py", "deploy/docker-compose.aliyun.yml", "deploy/aliyun.env.example", "src/components/xingbang/AgentNav.tsx", "src/components/xingbang/BusinessAssistant.tsx", "src/app/resetdata/layout.tsx", "src/app/resetdata/page.tsx", "src/app/resetdata/resetdata.css", "scripts/test_resetdata_ui.cjs", ) for relative in local_files: remote = posixpath.join(base, relative.replace("\\", "/")) try: sftp.stat(remote) run(client, f"cp -p {shlex.quote(remote)} {shlex.quote(remote + '.pre-deepseek-' + stamp)}") except FileNotFoundError: run(client, f"mkdir -p {shlex.quote(posixpath.dirname(remote))}") sftp.put(str(ROOT / relative), remote) env_path = posixpath.join(base, ".runtime.env") with sftp.file(env_path, "r") as handle: runtime_env = handle.read().decode("utf-8") original_runtime_env = runtime_env run(client, f"cp -p {shlex.quote(env_path)} {shlex.quote(env_path + '.pre-deepseek-' + stamp)}") runtime_env = update_env( runtime_env, { "DEEPSEEK_API_KEY": secret, "DEEPSEEK_BASE_URL": "https://api.deepseek.com", "DEEPSEEK_MODEL": "deepseek-chat", "XINGBANG_LOCAL_MODEL_KEY": secret, "XINGBANG_LOCAL_MODEL_URL": "https://api.deepseek.com", "XINGBANG_LOCAL_MODEL_NAME": "deepseek-chat", }, ) put_text(sftp, env_path, runtime_env, 0o600) probe = r''' import os import sys import httpx response = httpx.post( "https://api.deepseek.com/chat/completions", headers={"Authorization": "Bearer " + os.environ["DEEPSEEK_API_KEY"]}, json={"model": "deepseek-chat", "messages": [{"role": "user", "content": "Reply with OK."}], "max_tokens": 2, "temperature": 0}, timeout=30, trust_env=False, ) print("deepseek_official_status=" + str(response.status_code)) sys.exit(0 if response.status_code == 200 else 1) ''' encoded_probe = probe.encode("utf-8").hex() probe_command = ( f"cd {shlex.quote(base)} && docker run --rm --env-file .runtime.env " f"--entrypoint python {shlex.quote(current_image)} -c " + shlex.quote("exec(bytes.fromhex(" + repr(encoded_probe) + ").decode('utf-8'))") ) print(run(client, probe_command, timeout=90)) dockerfile = ( "FROM node:18-bookworm-slim AS frontend-builder\n" "WORKDIR /app/frontend\n" "COPY package*.json ./\n" "RUN npm ci\n" "COPY src ./src\n" "COPY next-env.d.ts next.config.mjs postcss.config.mjs tailwind.config.ts tsconfig.json ./\n" "RUN mkdir -p public && npm run build\n" f"FROM {current_image}\n" "WORKDIR /app\n" "COPY backend/services/assistant_model.py /app/backend/services/assistant_model.py\n" "COPY backend/services/project_assistant.py /app/backend/services/project_assistant.py\n" "COPY backend/api/risk_agent_config.py /app/backend/api/risk_agent_config.py\n" "RUN rm -rf /app/frontend/.next /app/frontend/public\n" "COPY --from=frontend-builder /app/frontend/.next /app/frontend/.next\n" "COPY --from=frontend-builder /app/frontend/public /app/frontend/public\n" ) dockerfile_path = posixpath.join(base, "Dockerfile.deepseek-official") put_text(sftp, dockerfile_path, dockerfile) print(run(client, f"cd {shlex.quote(base)} && docker build -f Dockerfile.deepseek-official -t {shlex.quote(new_image)} .", timeout=600)) state = posixpath.join(base, "state") mounts = ( ("workspace", "/app/backend/workspace"), ("config.json", "/app/backend/config.json"), ("tokenizer-cache", "/tmp/data-gym-cache"), ("xingbang_data", "/app/backend/xingbang_data"), ("logs", "/app/backend/logs"), ("sessions", "/app/backend/sessions"), ("raw_logs", "/app/backend/raw_logs"), ) mount_args = " ".join( "-v " + shlex.quote(posixpath.join(state, source) + ":" + target) for source, target in mounts ) launch = ( f"cd {shlex.quote(base)} && " "docker stop mini-openclaw >/dev/null && " f"docker rename mini-openclaw {shlex.quote(rollback_container)} && " f"docker run -d --name mini-openclaw --restart unless-stopped --env-file .runtime.env " f"-p {shlex.quote(port)}:3000 {mount_args} {shlex.quote(new_image)}" ) run(client, launch, timeout=120) healthy = False last = "unknown" for _ in range(45): last = run(client, "docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' mini-openclaw") if last == "healthy": healthy = True break if last in {"unhealthy", "exited", "dead"}: break time.sleep(4) if not healthy: run( client, "docker rm -f mini-openclaw >/dev/null 2>&1 || true; " f"docker rename {shlex.quote(rollback_container)} mini-openclaw; " "docker start mini-openclaw >/dev/null", ) put_text(sftp, env_path, original_runtime_env, 0o600) raise RuntimeError(f"New container health is {last}; previous container was restored") put_text(sftp, posixpath.join(base, ".deployed-version"), new_image + "\n") print(json.dumps({"status": "deployed", "image": new_image, "health": "healthy", "rollback_container": rollback_container})) return 0 finally: sftp.close() client.close() if __name__ == "__main__": raise SystemExit(main())