149 lines
5.9 KiB
Python
149 lines
5.9 KiB
Python
"""Read-only verification of the Alibaba Cloud demo runtime.
|
|
|
|
Connection secrets are read from the ignored server-deploy.conf file. Model
|
|
credentials are read inside the running container and are never returned.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import shlex
|
|
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 not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, value = line.split("=", 1)
|
|
values[key.strip()] = value.strip()
|
|
return values
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--config", type=Path, default=ROOT / "server-deploy.conf")
|
|
args = parser.parse_args()
|
|
config = load_config(args.config)
|
|
|
|
client = paramiko.SSHClient()
|
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
connect = {
|
|
"hostname": config["SERVER_HOST"],
|
|
"port": int(config.get("SERVER_PORT", "22")),
|
|
"username": config["SERVER_USER"],
|
|
"timeout": 15,
|
|
"banner_timeout": 15,
|
|
"auth_timeout": 15,
|
|
}
|
|
if config.get("SERVER_KEYFILE"):
|
|
connect["key_filename"] = config["SERVER_KEYFILE"]
|
|
else:
|
|
connect["password"] = config["SERVER_PASSWORD"]
|
|
client.connect(**connect)
|
|
|
|
container_script = r'''
|
|
import json
|
|
import os
|
|
|
|
import httpx
|
|
|
|
base = os.getenv("DEEPSEEK_BASE_URL", "").rstrip("/")
|
|
key = os.getenv("DEEPSEEK_API_KEY", "")
|
|
model = os.getenv("DEEPSEEK_MODEL", "")
|
|
local_url = os.getenv("XINGBANG_LOCAL_MODEL_URL", "").rstrip("/")
|
|
local_key = os.getenv("XINGBANG_LOCAL_MODEL_KEY", "")
|
|
local_model = os.getenv("XINGBANG_LOCAL_MODEL_NAME", "")
|
|
result = {
|
|
"deepseek_base_url": base,
|
|
"official_url": base == "https://api.deepseek.com",
|
|
"deepseek_model": model,
|
|
"deepseek_key_configured": bool(key),
|
|
"business_model_url": local_url,
|
|
"business_model_name": local_model,
|
|
"business_model_key_configured": bool(local_key),
|
|
"configured_api_status": None,
|
|
"deepseek_official_api_status": None,
|
|
}
|
|
if base and key:
|
|
try:
|
|
response = httpx.post(
|
|
base + "/chat/completions",
|
|
headers={"Authorization": "Bearer " + key},
|
|
json={
|
|
"model": model or "deepseek-chat",
|
|
"messages": [{"role": "user", "content": "Reply with OK."}],
|
|
"max_tokens": 2,
|
|
"temperature": 0,
|
|
},
|
|
timeout=30,
|
|
trust_env=False,
|
|
)
|
|
result["configured_api_status"] = response.status_code
|
|
except Exception as exc:
|
|
result["configured_api_status"] = "connection_error:" + type(exc).__name__
|
|
try:
|
|
response = httpx.post(
|
|
"https://api.deepseek.com/chat/completions",
|
|
headers={"Authorization": "Bearer " + key},
|
|
json={
|
|
"model": "deepseek-chat",
|
|
"messages": [{"role": "user", "content": "Reply with OK."}],
|
|
"max_tokens": 2,
|
|
"temperature": 0,
|
|
},
|
|
timeout=30,
|
|
trust_env=False,
|
|
)
|
|
result["deepseek_official_api_status"] = response.status_code
|
|
except Exception as exc:
|
|
result["deepseek_official_api_status"] = "connection_error:" + type(exc).__name__
|
|
print(json.dumps(result, ensure_ascii=True))
|
|
'''
|
|
encoded = container_script.encode("utf-8").hex()
|
|
remote_python = (
|
|
"import os;exec(bytes.fromhex(" + repr(encoded) + ").decode('utf-8'))"
|
|
)
|
|
command = (
|
|
"set -eu; "
|
|
"printf 'container='; docker inspect -f '{{.State.Status}}' mini-openclaw; "
|
|
"printf 'health='; docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' mini-openclaw; "
|
|
"printf 'image='; docker inspect -f '{{.Config.Image}}' mini-openclaw; "
|
|
"printf 'restart_policy='; docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' mini-openclaw; "
|
|
"printf 'port_bindings='; docker inspect -f '{{json .HostConfig.PortBindings}}' mini-openclaw; "
|
|
"printf 'mounts='; docker inspect -f '{{range .Mounts}}{{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}}:{{.Destination}};{{end}}' mini-openclaw; "
|
|
"printf 'networks='; docker inspect -f '{{range $name, $value := .NetworkSettings.Networks}}{{$name}};{{end}}' mini-openclaw; "
|
|
"printf 'deploy_path='; test -d " + shlex.quote(config.get("SERVER_DEPLOY_PATH", "")) + " && echo present || echo missing; "
|
|
"printf 'compose_files='; docker inspect -f '{{index .Config.Labels \"com.docker.compose.project.config_files\"}}' mini-openclaw 2>/dev/null || true; "
|
|
"printf 'working_dir='; docker inspect -f '{{index .Config.Labels \"com.docker.compose.project.working_dir\"}}' mini-openclaw 2>/dev/null || true; "
|
|
"cd " + shlex.quote(config.get("SERVER_DEPLOY_PATH", "/")) + "; "
|
|
"if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then "
|
|
"printf 'git_head='; git rev-parse --short HEAD; "
|
|
"printf 'git_branch='; git branch --show-current; "
|
|
"printf 'git_dirty_files='; git status --porcelain | wc -l; "
|
|
"else echo 'git_head=not-a-repository'; fi; "
|
|
"echo 'deploy_files_begin'; find . -maxdepth 2 -type f -printf '%P\\n' | sort | head -80; echo 'deploy_files_end'; "
|
|
"docker exec mini-openclaw python -c " + shlex.quote(remote_python)
|
|
)
|
|
_, stdout, stderr = client.exec_command(command, timeout=60)
|
|
output = stdout.read().decode("utf-8", errors="replace")
|
|
error = stderr.read().decode("utf-8", errors="replace")
|
|
code = stdout.channel.recv_exit_status()
|
|
client.close()
|
|
if code:
|
|
raise RuntimeError(error.strip() or f"Remote verification failed with exit code {code}")
|
|
print(output.strip())
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|