furniture-3d-system-Wu/scripts/yydd-prod-release-guards.sh
WuYongJie 928a7b37a9 feat(ai): use durable Relay native image tasks
Persist image submissions with fixed idempotency keys, leased execution, provider
identity, reference snapshots and staged results. Integrate DIY, assisted assembly
and non-integrated white-background sessions without holding generation in a
browser request. Recover ambiguous submissions through owner-scoped reads and
stop a new eight-view batch when restoring an earlier request.

Add the NativeImageGenerationJob migration and a dedicated worker to both release
entry points. Protect job inputs/results from storage GC. Existing production
configuration and business data are not changed by this commit.

Validation: 256 focused Jest tests, isolated PostgreSQL migration/lease/transaction
checks (8), deployment guard tests (9), type-check, production build and desktop/
mobile browser component scenarios. Live native 2K reference-image request worked;
reusing its idempotency key returned the same task. Build uses local placeholders
for otherwise absent Drawing System configuration. Three unrelated material tests
also fail with the original mainline test file; they remain outside this change.

Architecture/visual/root-cause review: passed with warnings. Independent review
completed for the integration and shared input/storage paths; worker additionally
reviewed by the primary agent. Production merge/deployment requires authorization.
2026-10-15 14:55:46 +08:00

144 lines
5.1 KiB
Bash

# Embedded by both official deployment entry points; no remote helper dependency.
ensure_ai_image_worker_secret() {
local app_root="$1"
[ -f "$app_root/scripts/run-ai-image-worker.mjs" ] || return 0
if ! grep -qE '^AI_IMAGE_WORKER_SECRET=.+$' "$SHARED/.env"; then
local secret
secret="$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('hex'))")" || return 1
[ -n "$secret" ] || return 1
(umask 077; printf '\nAI_IMAGE_WORKER_SECRET=%s\n' "$secret" >> "$SHARED/.env")
chmod 600 "$SHARED/.env"
fi
}
start_ai_image_worker() {
local app_root="$1"
local name="${PM2_NAME}-ai-image-worker"
pm2 delete "$name" >/dev/null 2>&1 || true
# Rolling back to an older release pauses the new queue without losing persisted jobs.
[ -f "$app_root/scripts/run-ai-image-worker.mjs" ] || return 0
local command="cd '$app_root' && set -a && source '$SHARED/.env' && set +a && exec env PORT=$APP_PORT node scripts/run-ai-image-worker.mjs"
pm2 start /bin/bash --name "$name" -- -lc "$command"
}
assert_release_ready() {
local app_root="$1"
local runtime="$app_root/.next/standalone"
if [ ! -s "$runtime/server.js" ] || [ ! -d "$runtime/.next/static" ]; then
echo "Incomplete standalone release: $app_root" >&2
return 1
fi
if [ -z "$(find "$runtime/.next/static" -type f -size +0c -print -quit)" ]; then
echo "Empty standalone static assets: $app_root" >&2
return 1
fi
}
prepare_standalone_runtime() {
local app_root="$1"
local runtime="$app_root/.next/standalone"
if [ ! -s "$runtime/server.js" ] || [ ! -d "$app_root/.next/static" ]; then
echo "Missing build output: $app_root" >&2
return 1
fi
if [ -z "$(find "$app_root/.next/static" -type f -size +0c -print -quit)" ]; then
echo "Empty build static assets: $app_root" >&2
return 1
fi
mkdir -p "$runtime/.next"
rm -rf -- "$runtime/.next/static" "$runtime/public"
cp -a "$app_root/.next/static" "$runtime/.next/static"
if [ -d "$app_root/public" ]; then
cp -a "$app_root/public" "$runtime/public"
fi
# Turbopack standalone tracing may omit onnxruntime-node native binaries;
# restore them or @huggingface/transformers fails to load libonnxruntime at runtime.
local ort_real="$app_root/node_modules/onnxruntime-node"
local ort_runtime="$runtime/node_modules/onnxruntime-node"
if [ -d "$ort_real/bin" ] && [ ! -L "$ort_runtime" ]; then
mkdir -p "$ort_runtime"
rm -rf -- "$ort_runtime/bin"
cp -a "$ort_real/bin" "$ort_runtime/bin"
fi
assert_release_ready "$app_root"
}
acquire_deploy_lock() {
command -v python3 >/dev/null || return 1
exec 9>"$BASE/.deploy.lock"
if ! flock -n 9; then
echo "Another production deployment is running" >&2
return 1
fi
}
verify_release_http() {
python3 - "$1" <<'PY'
import json
import sys
import time
from html.parser import HTMLParser
from urllib.parse import urljoin, urlparse
from urllib.request import urlopen
base = sys.argv[1].rstrip('/')
def get(url):
with urlopen(url, timeout=15) as response:
if response.status != 200:
raise RuntimeError('HTTP %s: %s' % (response.status, url))
return response.read(), response.headers.get_content_type(), response.url
for attempt in range(10):
try:
payload, _, _ = get(base + '/api/health')
result = json.loads(payload)
health = result.get('data', {}).get('services', {})
if not result.get('success'):
raise RuntimeError('Health request failed')
if health.get('database') != 'connected' or health.get('api') != 'running':
raise RuntimeError('Unhealthy database or API')
break
except Exception:
if attempt == 9:
raise
time.sleep(2)
class Assets(HTMLParser):
def __init__(self):
super().__init__()
self.urls = set()
def handle_starttag(self, tag, attrs):
attrs = dict(attrs)
if tag == 'script' and attrs.get('src'):
self.urls.add(attrs['src'])
if tag == 'link' and 'stylesheet' in attrs.get('rel', '').split() and attrs.get('href'):
self.urls.add(attrs['href'])
assets = set()
for path in ['/', '/installation-demo-workbench']:
content, kind, final_url = get(base + path)
if kind != 'text/html':
raise RuntimeError('Expected HTML: ' + path)
parser = Assets()
parser.feed(content.decode('utf-8'))
found = {urljoin(final_url, item) for item in parser.urls}
found = {item for item in found if urlparse(item).path.startswith('/_next/static/')}
if not found:
raise RuntimeError('No Next static references: ' + path)
assets.update(found)
for url in sorted(assets):
content, kind, _ = get(url)
if not content or kind == 'text/html':
raise RuntimeError('Invalid static asset: ' + url)
print('200 ' + url, flush=True)
if not any(urlparse(url).path.endswith('.css') for url in assets):
raise RuntimeError('No stylesheet verified')
if not any(urlparse(url).path.endswith('.js') for url in assets):
raise RuntimeError('No JavaScript verified')
print('Health and %d static assets verified at %s' % (len(assets), base), flush=True)
PY
}