feat: release v1.0.1 CADWorld 网站与 LeKiwi 智能抓放
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
*
|
||||
!decision_server/
|
||||
!decision_server/**
|
||||
decision_server/tests/
|
||||
**/__pycache__/
|
||||
**/*.pyc
|
||||
!contracts/
|
||||
!contracts/lekiwi-agent-v1.schema.json
|
||||
!vendor/
|
||||
!vendor/codex
|
||||
!vendor/wheels/
|
||||
!vendor/wheels/*.whl
|
||||
!deploy/
|
||||
!deploy/cadworld/
|
||||
!deploy/cadworld/requirements.lock.txt
|
||||
!Dockerfile
|
||||
@@ -0,0 +1,13 @@
|
||||
# Imported and verified from images.lock.json; never pull during production builds.
|
||||
FROM cadworld-python-base:locked
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
|
||||
WORKDIR /app
|
||||
COPY vendor/wheels/ /opt/wheels/
|
||||
COPY deploy/cadworld/requirements.lock.txt /opt/requirements.lock.txt
|
||||
RUN pip install --no-index --find-links=/opt/wheels --require-hashes -r /opt/requirements.lock.txt && pip check
|
||||
COPY vendor/codex /usr/local/bin/codex
|
||||
RUN chmod 755 /usr/local/bin/codex && codex --version && useradd --uid 10001 --no-create-home --shell /usr/sbin/nologin cadworld
|
||||
COPY decision_server/ /app/decision_server/
|
||||
COPY contracts/lekiwi-agent-v1.schema.json /app/contracts/lekiwi-agent-v1.schema.json
|
||||
USER 10001:10001
|
||||
ENTRYPOINT ["python", "-m", "decision_server"]
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-time installation. Existing domain configuration is never silently overwritten.
|
||||
set -euo pipefail
|
||||
root=/opt/cadworld-sim
|
||||
base=/opt/1panel/apps/openresty/openresty
|
||||
conf=$base/conf/conf.d/cadworld-sim.robotquan.com.conf
|
||||
source_dir=$(cd -- "$(dirname -- "$0")" && pwd)
|
||||
[[ ! -e "$conf" ]] || { echo 'Domain vhost already exists; inspect/backup it first.' >&2; exit 1; }
|
||||
mkdir -p "$root/backups" "$base/www/acme/cadworld-sim.robotquan.com" "$base/www/ssl/cadworld-sim.robotquan.com"
|
||||
chmod 700 "$root/backups" "$base/www/ssl/cadworld-sim.robotquan.com"
|
||||
tar -czf "$root/backups/openresty-pre-cadworld.tar.gz" -C "$base" conf
|
||||
cp "$source_dir/compose.yaml" "$root/compose.yaml"
|
||||
printf 'CADWORLD_RELEASE=bootstrap\n' > "$root/release.env"
|
||||
# HTTP only serves challenges. No API or credentials until a trusted certificate exists.
|
||||
printf '%s\n' \
|
||||
'server {' \
|
||||
' listen 80; server_name cadworld-sim.robotquan.com;' \
|
||||
' location ^~ /.well-known/acme-challenge/ { root /www/acme/cadworld-sim.robotquan.com; default_type text/plain; }' \
|
||||
' location / { return 503; }' \
|
||||
'}' > "$conf"
|
||||
docker exec 1Panel-openresty-m72w nginx -t
|
||||
docker exec 1Panel-openresty-m72w nginx -s reload
|
||||
docker compose --env-file "$root/release.env" -f "$root/compose.yaml" run --rm -T --interactive=false certbot certonly \
|
||||
--non-interactive --agree-tos --register-unsafely-without-email \
|
||||
--webroot -w /var/www/acme -d cadworld-sim.robotquan.com
|
||||
cp "$source_dir/openresty.conf" "$conf"
|
||||
docker exec 1Panel-openresty-m72w nginx -t
|
||||
docker exec 1Panel-openresty-m72w nginx -s reload
|
||||
cp "$source_dir/renew-certificate.sh" "$root/renew-certificate.sh"
|
||||
chmod 700 "$root/renew-certificate.sh"
|
||||
cp "$source_dir/cadworld-certificate.service" /etc/systemd/system/
|
||||
cp "$source_dir/cadworld-certificate.timer" /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now cadworld-certificate.timer
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Renew CADWorld Sim TLS certificate
|
||||
After=docker.service network-online.target
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/opt/cadworld-sim/renew-certificate.sh
|
||||
TimeoutStartSec=10min
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Twice daily CADWorld TLS renewal check
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03,15:00:00
|
||||
RandomizedDelaySec=1h
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,58 @@
|
||||
name: cadworld-sim
|
||||
services:
|
||||
decision:
|
||||
image: cadworld-decision:${CADWORLD_RELEASE:?release tag required}
|
||||
command:
|
||||
- --website-origin
|
||||
- https://cadworld-sim.robotquan.com
|
||||
- --bind
|
||||
- 0.0.0.0
|
||||
- --trusted-proxy
|
||||
- 172.30.88.1
|
||||
ports:
|
||||
- '127.0.0.1:8768:8768'
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:rw,noexec,nosuid,size=268435456,mode=1777
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
mem_limit: 2g
|
||||
cpus: 2
|
||||
pids_limit: 128
|
||||
stop_grace_period: 20s
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
CMD,
|
||||
python,
|
||||
-c,
|
||||
"import urllib.request; r=urllib.request.Request('http://127.0.0.1:8768/healthz',headers={'Host':'cadworld-sim.robotquan.com'}); urllib.request.urlopen(r,timeout=3).read()",
|
||||
]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
logging:
|
||||
driver: json-file
|
||||
options: { max-size: 5m, max-file: '3' }
|
||||
networks: [cadworld]
|
||||
certbot:
|
||||
image: cadworld-certbot:locked
|
||||
profiles: [maintenance]
|
||||
volumes:
|
||||
- /opt/1panel/apps/openresty/openresty/www/acme/cadworld-sim.robotquan.com:/var/www/acme
|
||||
- /opt/1panel/apps/openresty/openresty/www/ssl/cadworld-sim.robotquan.com:/etc/letsencrypt
|
||||
tmpfs:
|
||||
- /var/lib/letsencrypt
|
||||
- /var/log/letsencrypt
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
networks: [cadworld]
|
||||
networks:
|
||||
cadworld:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.30.88.0/24
|
||||
gateway: 172.30.88.1
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"platform": "linux/amd64",
|
||||
"crane": "v0.20.6",
|
||||
"python": "docker.io/library/python@sha256:1aaa65a85fda306ffb8b910824d4e93bdce61e212c7e87168123ea3073b41a1a",
|
||||
"certbot": "docker.io/certbot/certbot@sha256:398c47284a6d6782825be71685f677ef3a1e65b8b5c278a8b1e99f6da84b4eb9",
|
||||
"codex": "@openai/codex@0.147.0-linux-x64"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Archive hashes must be verified before running this script.
|
||||
set -euo pipefail
|
||||
images=${1:?usage: import-images.sh IMAGE_DIRECTORY}
|
||||
docker load -i "$images/python.tar"
|
||||
docker tag sha256:febd0be41adb897a0ab8f1f1c693d8912669ea60c4940e076e9946b60e210ef0 cadworld-python-base:locked
|
||||
docker load -i "$images/certbot.tar"
|
||||
docker tag sha256:d9a5b0cd892677dd43fa6f60adba9c06f68d7fd1da09b04f15a7b0f468e4c3f8 cadworld-certbot:locked
|
||||
@@ -0,0 +1,62 @@
|
||||
# Managed by CADWorld deployment, not a replacement for 1Panel's main config.
|
||||
limit_req_zone $binary_remote_addr zone=cadworld_api:10m rate=10r/s;
|
||||
server {
|
||||
listen 80;
|
||||
server_name cadworld-sim.robotquan.com;
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /www/acme/cadworld-sim.robotquan.com;
|
||||
default_type text/plain;
|
||||
}
|
||||
location / { return 301 https://cadworld-sim.robotquan.com$request_uri; }
|
||||
}
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name cadworld-sim.robotquan.com;
|
||||
ssl_certificate /www/ssl/cadworld-sim.robotquan.com/live/cadworld-sim.robotquan.com/fullchain.pem;
|
||||
ssl_certificate_key /www/ssl/cadworld-sim.robotquan.com/live/cadworld-sim.robotquan.com/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_session_cache shared:CADWorldTLS:10m;
|
||||
ssl_session_timeout 1d;
|
||||
root /www/sites/cadworld-sim.robotquan.com/current;
|
||||
index index.html;
|
||||
autoindex off;
|
||||
client_max_body_size 64k;
|
||||
client_body_timeout 15s;
|
||||
keepalive_timeout 30s;
|
||||
server_tokens off;
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header Referrer-Policy no-referrer always;
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'none'" always;
|
||||
# Hash assets cache; all headers remain inherited (no nested add_header).
|
||||
set $cadworld_cache "no-cache";
|
||||
if ($uri ~ ^/assets/) { set $cadworld_cache "public, max-age=31536000, immutable"; }
|
||||
if ($uri ~ ^/pyodide/) { set $cadworld_cache "public, max-age=86400"; }
|
||||
if ($uri ~ ^/api/) { set $cadworld_cache "no-store"; }
|
||||
add_header Cache-Control $cadworld_cache always;
|
||||
location ^~ /api/decision/v1/ {
|
||||
access_log off;
|
||||
limit_req zone=cadworld_api burst=20 nodelay;
|
||||
limit_req_status 429;
|
||||
proxy_pass http://127.0.0.1:8768;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host cadworld-sim.robotquan.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Forwarded "";
|
||||
proxy_set_header Connection "";
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_read_timeout 70s;
|
||||
proxy_send_timeout 15s;
|
||||
proxy_buffering off;
|
||||
proxy_hide_header Cache-Control;
|
||||
}
|
||||
location ^~ /api/ { return 404; }
|
||||
location ^~ /physics/ { return 404; }
|
||||
location = /tuning.html { return 404; }
|
||||
location ~ /\. { return 404; }
|
||||
location ~ \.wasm$ { types { application/wasm wasm; } try_files $uri =404; }
|
||||
location / { try_files $uri $uri/ =404; }
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Package an explicit allowlist, never the working tree/.env/user assets wholesale."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("release")
|
||||
args = parser.parse_args()
|
||||
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,63}", args.release):
|
||||
parser.error("invalid release")
|
||||
output = ROOT / "build/website-deployment/bundles" / args.release
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
|
||||
def copy(source, target):
|
||||
if source.is_symlink():
|
||||
raise RuntimeError("symlinks not allowed in publish inputs")
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(source, target)
|
||||
|
||||
for source in (ROOT / "decision_server").rglob("*"):
|
||||
relative = source.relative_to(ROOT)
|
||||
if (
|
||||
source.is_file()
|
||||
and "tests" not in relative.parts
|
||||
and "__pycache__" not in relative.parts
|
||||
and source.suffix in (".py", ".json", ".txt", ".md")
|
||||
):
|
||||
copy(source, output / "app" / relative)
|
||||
copy(
|
||||
ROOT / "contracts/lekiwi-agent-v1.schema.json",
|
||||
output / "app/contracts/lekiwi-agent-v1.schema.json",
|
||||
)
|
||||
for source in (ROOT / "deploy/cadworld").iterdir():
|
||||
if source.is_file():
|
||||
copy(source, output / "deploy" / source.name)
|
||||
copy(ROOT / "deploy/cadworld/Dockerfile", output / "app/Dockerfile")
|
||||
copy(ROOT / "deploy/cadworld/.dockerignore", output / "app/.dockerignore")
|
||||
copy(
|
||||
ROOT / "deploy/cadworld/requirements.lock.txt",
|
||||
output / "app/deploy/cadworld/requirements.lock.txt",
|
||||
)
|
||||
vendor = ROOT / "build/website-deployment/vendor"
|
||||
for source in [
|
||||
vendor / "codex",
|
||||
vendor / "codex-origin.json",
|
||||
*sorted((vendor / "wheels").glob("*.whl")),
|
||||
]:
|
||||
copy(source, output / "app/vendor" / source.relative_to(vendor))
|
||||
for source in (ROOT / "website-dist").rglob("*"):
|
||||
if source.is_file():
|
||||
copy(source, output / "static" / source.relative_to(ROOT / "website-dist"))
|
||||
if not (output / "static/index.html").exists() or (output / "static/tuning.html").exists():
|
||||
raise RuntimeError("website production build required")
|
||||
manifest = {
|
||||
str(p.relative_to(output)): hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
for p in sorted(output.rglob("*"))
|
||||
if p.is_file()
|
||||
}
|
||||
(output / "manifest.json").write_text(
|
||||
json.dumps({"release": args.release, "files": manifest}, indent=2)
|
||||
)
|
||||
archive = output.with_suffix(".tar.gz")
|
||||
with tarfile.open(archive, "w:gz") as tar:
|
||||
for path in sorted(output.iterdir()):
|
||||
tar.add(path, arcname=path.name)
|
||||
print(str(archive))
|
||||
print("sha256", hashlib.sha256(archive.read_bytes()).hexdigest())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run on the target host after baseline backup, image imports and TLS bootstrap.
|
||||
set -euo pipefail
|
||||
release=${1:?usage: publish.sh RELEASE}
|
||||
[[ "$release" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$ ]] || exit 2
|
||||
root=/opt/cadworld-sim
|
||||
www=/opt/1panel/apps/openresty/openresty/www
|
||||
site=$www/sites/cadworld-sim.robotquan.com
|
||||
nginx=1Panel-openresty-m72w
|
||||
stage=$root/releases/$release
|
||||
[[ -f "$stage/manifest.json" ]] || { echo 'missing verified release'; exit 1; }
|
||||
python3 - "$stage" <<'PY'
|
||||
import hashlib,json,pathlib,sys
|
||||
root=pathlib.Path(sys.argv[1])
|
||||
manifest=json.loads((root/'manifest.json').read_text())
|
||||
for name,digest in manifest['files'].items():
|
||||
path=root/name
|
||||
if '..' in pathlib.Path(name).parts or pathlib.Path(name).is_absolute() or path.is_symlink():
|
||||
raise RuntimeError('invalid manifest path')
|
||||
if hashlib.sha256(path.read_bytes()).hexdigest()!=digest:
|
||||
raise RuntimeError('artifact checksum mismatch: '+name)
|
||||
print('Release manifest verified')
|
||||
PY
|
||||
old=''
|
||||
[[ ! -f $root/current.txt ]] || read -r old < "$root/current.txt"
|
||||
docker build --network none --pull=false -t "cadworld-decision:$release" "$stage/app"
|
||||
# No current traffic is changed before the build and static manifest succeed.
|
||||
mkdir -p "$site/releases/$release"
|
||||
cp -a "$stage/static/." "$site/releases/$release/"
|
||||
chmod -R a+rX "$site/releases/$release"
|
||||
restore() {
|
||||
trap - ERR
|
||||
if [[ -n "$old" ]]; then
|
||||
cp "$root/releases/$old/deploy/compose.yaml" "$root/compose.yaml"
|
||||
cp "$root/releases/$old/deploy/openresty.conf" /opt/1panel/apps/openresty/openresty/conf/conf.d/cadworld-sim.robotquan.com.conf
|
||||
printf 'CADWORLD_RELEASE=%s\n' "$old" > "$root/release.env"
|
||||
docker compose --env-file "$root/release.env" -f "$root/compose.yaml" up -d --wait decision || true
|
||||
ln -sfn "releases/$old" "$site/current.next"
|
||||
mv -Tf "$site/current.next" "$site/current"
|
||||
docker exec "$nginx" nginx -t && docker exec "$nginx" nginx -s reload || true
|
||||
else
|
||||
docker compose --env-file "$root/release.env" -f "$root/compose.yaml" stop decision || true
|
||||
conf=/opt/1panel/apps/openresty/openresty/conf/conf.d/cadworld-sim.robotquan.com.conf
|
||||
[[ ! -f "$conf" ]] || mv "$conf" "$root/failed-first-vhost.conf"
|
||||
docker exec "$nginx" nginx -t && docker exec "$nginx" nginx -s reload || true
|
||||
fi
|
||||
echo 'Publish failed; previous deployment retained/restored. Inspect logs before retry.' >&2
|
||||
exit 1
|
||||
}
|
||||
trap restore ERR
|
||||
cp "$stage/deploy/compose.yaml" "$root/compose.yaml"
|
||||
printf 'CADWORLD_RELEASE=%s\n' "$release" > "$root/release.env"
|
||||
docker compose --env-file "$root/release.env" -f "$root/compose.yaml" up -d --wait --wait-timeout 75 decision
|
||||
curl --fail --silent --show-error --max-time 5 -H 'Host: cadworld-sim.robotquan.com' http://127.0.0.1:8768/healthz
|
||||
ln -sfn "releases/$release" "$site/current.next"
|
||||
mv -Tf "$site/current.next" "$site/current"
|
||||
cp "$stage/deploy/openresty.conf" /opt/1panel/apps/openresty/openresty/conf/conf.d/cadworld-sim.robotquan.com.conf
|
||||
docker exec "$nginx" nginx -t
|
||||
docker exec "$nginx" nginx -s reload
|
||||
curl --fail --silent --show-error --max-time 15 https://cadworld-sim.robotquan.com/ -o /dev/null
|
||||
printf '%s\n' "$old" > "$root/previous.txt"
|
||||
printf '%s\n' "$release" > "$root/current.txt"
|
||||
docker image inspect "cadworld-decision:$release" --format '{{.Id}}' > "$stage/image-id.txt"
|
||||
trap - ERR
|
||||
echo "Published $release; previous=$old"
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
root=/opt/cadworld-sim
|
||||
docker compose --env-file "$root/release.env" -f "$root/compose.yaml" run --rm -T --interactive=false certbot renew --non-interactive --no-random-sleep-on-renew "$@"
|
||||
docker exec 1Panel-openresty-m72w nginx -t
|
||||
docker exec 1Panel-openresty-m72w nginx -s reload
|
||||
@@ -0,0 +1,11 @@
|
||||
# Generated for CPython 3.12 Linux amd64; verified offline wheels.
|
||||
aiohappyeyeballs==2.7.1 --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472
|
||||
aiohttp==3.14.3 --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42
|
||||
aiosignal==1.4.0 --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e
|
||||
attrs==26.1.0 --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309
|
||||
frozenlist==1.8.0 --hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383
|
||||
idna==3.20 --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c
|
||||
multidict==6.9.1 --hash=sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d
|
||||
propcache==0.5.4 --hash=sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b
|
||||
typing_extensions==4.16.0 --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8
|
||||
yarl==1.25.1 --hash=sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Operator-invoked production recovery check; creates no model credentials or calls."""
|
||||
|
||||
import http.client
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
HOST = "cadworld-sim.robotquan.com"
|
||||
CONTAINER = "cadworld-sim-decision-1"
|
||||
|
||||
|
||||
def request(path, method="GET", headers=None, body=None):
|
||||
connection = http.client.HTTPSConnection(HOST, timeout=10)
|
||||
connection.request(method, path, body=body, headers=headers or {})
|
||||
response = connection.getresponse()
|
||||
status, cookie = response.status, response.getheader("Set-Cookie")
|
||||
response.read()
|
||||
connection.close()
|
||||
return status, cookie
|
||||
|
||||
|
||||
def restart_count():
|
||||
return int(
|
||||
subprocess.check_output(
|
||||
["docker", "inspect", CONTAINER, "--format", "{{.RestartCount}}"],
|
||||
universal_newlines=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
status, cookie = request(
|
||||
"/api/decision/v1/session",
|
||||
"POST",
|
||||
{"Origin": "https://" + HOST, "Content-Type": "application/json"},
|
||||
"{}",
|
||||
)
|
||||
if status != 200 or not cookie:
|
||||
raise RuntimeError("session bootstrap failed")
|
||||
cookie = cookie.split(";", 1)[0] # Never print or persist the credential.
|
||||
before = restart_count()
|
||||
# Kill only this application's Python worker, not Docker/1Panel/host PID 1.
|
||||
code = """import os,signal
|
||||
for ident in os.listdir('/proc'):
|
||||
if ident.isdigit():
|
||||
try:
|
||||
raw=open('/proc/'+ident+'/cmdline','rb').read()
|
||||
except OSError:
|
||||
continue
|
||||
if raw.startswith(b'python\\x00-m\\x00decision_server\\x00'):
|
||||
os.kill(int(ident),signal.SIGKILL)
|
||||
break
|
||||
else:
|
||||
raise RuntimeError('worker not found')
|
||||
"""
|
||||
# Tini may terminate the exec probe together with the killed worker (137).
|
||||
probe = subprocess.run(["docker", "exec", CONTAINER, "python", "-c", code], check=False)
|
||||
if probe.returncode not in (0, 137):
|
||||
raise RuntimeError("worker crash probe failed")
|
||||
deadline = time.monotonic() + 60
|
||||
while time.monotonic() < deadline:
|
||||
time.sleep(1)
|
||||
try:
|
||||
status, _ = request("/api/decision/v1/status", headers={"Cookie": cookie})
|
||||
if status == 401 and restart_count() > before:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"automaticRestart": True,
|
||||
"oldSessionStatus": status,
|
||||
"restartCount": restart_count(),
|
||||
"modelCalls": 0,
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
raise RuntimeError("automatic recovery not verified")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user