Files
mini-openclaw/scripts/deploy_aliyun_deepseek.py
T
2026-09-28 16:34:10 +08:00

238 lines
9.5 KiB
Python

"""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())