feat: add repeatable Pascal production deployment
This commit is contained in:
parent
12ae15ed3a
commit
af3d17f4a2
@ -3,7 +3,7 @@
|
||||
|
||||
location = /_pascal_auth {
|
||||
internal;
|
||||
proxy_pass http://127.0.0.1:3125/api/internal/pascal/access;
|
||||
proxy_pass http://__MAIN_NGINX_UPSTREAM__/api/internal/pascal/access;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header Host $host;
|
||||
@ -13,15 +13,15 @@ location = /_pascal_auth {
|
||||
proxy_set_header X-Pascal-Proxy-Secret "__PASCAL_PROXY_SECRET__";
|
||||
}
|
||||
|
||||
location = /pascal-service {
|
||||
return 302 /pascal-service/scenes;
|
||||
location = __PASCAL_BASE_PATH__ {
|
||||
return 302 __PASCAL_BASE_PATH__/scenes;
|
||||
}
|
||||
|
||||
location ^~ /pascal-service/ {
|
||||
location ^~ __PASCAL_BASE_PATH__/ {
|
||||
auth_request /_pascal_auth;
|
||||
auth_request_set $pascal_user_id $upstream_http_x_pascal_user_id;
|
||||
|
||||
proxy_pass http://127.0.0.1:3002;
|
||||
proxy_pass http://__PASCAL_NGINX_UPSTREAM__;
|
||||
proxy_http_version 1.1;
|
||||
proxy_hide_header X-Powered-By;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
@ -115,6 +115,11 @@ and provider from system settings on every request. OpenAI-compatible
|
||||
`openai`, `custom`, `alibaba`, and `zhipu` providers work without copying their
|
||||
API keys into the Pascal service.
|
||||
|
||||
For repeatable deployment and upgrades on additional servers, use
|
||||
`scripts/deploy-pascal-production.sh`. The complete defaults, override
|
||||
variables, health check, real-model smoke test, and rollback behavior are
|
||||
documented in `docs/pascal-production-deployment.md`.
|
||||
|
||||
## Next Step
|
||||
|
||||
After the iframe POC is stable, the native integration path is:
|
||||
|
||||
111
docs/pascal-production-deployment.md
Normal file
111
docs/pascal-production-deployment.md
Normal file
@ -0,0 +1,111 @@
|
||||
# Pascal Editor 可复用生产部署
|
||||
|
||||
本项目把 Pascal Editor 作为独立 Bun/Next.js 服务运行,并通过主站 Nginx
|
||||
同域挂载。部署脚本会复用主站登录态、系统设置中的 Agent 文本模型和主站
|
||||
Git 仓库里的 `pascal-editor-ai-agent` 独立分支。
|
||||
|
||||
## 默认结构
|
||||
|
||||
| 项目 | 默认值 |
|
||||
| --- | --- |
|
||||
| 主站目录 | 当前仓库目录 |
|
||||
| 主站 PM2 服务 | `ai-chatbot` |
|
||||
| 主站内部地址 | `http://127.0.0.1:3125` |
|
||||
| Pascal 源码 | `/opt/pascal-editor` |
|
||||
| Pascal 数据 | `/var/lib/pascal-editor` |
|
||||
| Pascal PM2 服务 | `pascal-editor` |
|
||||
| Pascal 监听 | `127.0.0.1:3002` |
|
||||
| 同域路径 | `/pascal-service` |
|
||||
| Pascal Git 分支 | `pascal-editor-ai-agent` |
|
||||
|
||||
Pascal 端口不对公网开放。Nginx 会先通过主站
|
||||
`/api/internal/pascal/access` 校验登录 Cookie,再把可信用户 ID 注入 Pascal。
|
||||
场景的创建、列表、读取、保存、删除、事件流和 AI Agent 都检查同一个
|
||||
`ownerId`。
|
||||
|
||||
## 首次部署
|
||||
|
||||
在目标服务器先部署好主站并确认主站 Git remote 能读取
|
||||
`pascal-editor-ai-agent` 分支,然后执行:
|
||||
|
||||
```bash
|
||||
cd /var/www/ai-chatbot
|
||||
git pull --ff-only origin main
|
||||
|
||||
sudo env \
|
||||
PASCAL_PUBLIC_ORIGIN=https://example.com \
|
||||
NGINX_SITE_CONFIG=/etc/nginx/sites-enabled/example.com.conf \
|
||||
pnpm pascal:deploy
|
||||
```
|
||||
|
||||
脚本会按顺序:
|
||||
|
||||
1. 生成或复用 `PASCAL_PROXY_SECRET`,写入主站 `.env.local`。
|
||||
2. 构建并重启主站。
|
||||
3. 安装固定版本 Bun(默认 `1.3.0`)。
|
||||
4. 克隆或快进 Pascal 独立分支。
|
||||
5. 写入仅 root 可读的 Pascal 生产环境文件。
|
||||
6. 安装依赖、构建 Pascal,并注册 PM2 服务。
|
||||
7. 渲染 Nginx 片段、执行 `nginx -t`,成功后热重载。
|
||||
8. 检查主站、Pascal、Agent 状态和主站模型网关。
|
||||
|
||||
主站环境文件和 Nginx 配置会备份到 `/var/backups/pascal-deploy`。Nginx
|
||||
校验失败时,脚本会立即恢复执行前的配置。
|
||||
|
||||
## 日常更新
|
||||
|
||||
主站和 Pascal 分支有新提交后,重复执行同一个命令即可。脚本只允许 Git
|
||||
快进更新;如果 `/opt/pascal-editor` 有本地修改,会停止部署,不会覆盖现场
|
||||
文件。
|
||||
|
||||
只做无模型费用的健康检查:
|
||||
|
||||
```bash
|
||||
sudo env \
|
||||
PASCAL_PUBLIC_ORIGIN=https://example.com \
|
||||
pnpm pascal:check
|
||||
```
|
||||
|
||||
执行一次真实自然语言建模、验证节点已落库并自动删除临时场景:
|
||||
|
||||
```bash
|
||||
sudo env PASCAL_SMOKE_CONFIRM=1 pnpm pascal:smoke
|
||||
```
|
||||
|
||||
真实冒烟会调用当前系统设置中的 Agent 模型,因此默认不会在一键部署时
|
||||
执行。需要每次部署自动执行时设置 `RUN_AGENT_SMOKE_TEST=true`。
|
||||
|
||||
## 常用覆盖项
|
||||
|
||||
| 环境变量 | 用途 |
|
||||
| --- | --- |
|
||||
| `MAIN_APP_DIR` | 主站源码目录 |
|
||||
| `MAIN_ENV_FILE` | 主站环境文件 |
|
||||
| `MAIN_PM2_NAME` | 主站 PM2 服务名 |
|
||||
| `MAIN_INTERNAL_URL` | Pascal 调用主站网关的地址 |
|
||||
| `MAIN_NGINX_UPSTREAM` | Nginx 主站 upstream,例如 `127.0.0.1:3125` |
|
||||
| `BUILD_MAIN_APP=false` | 跳过主站安装、构建和重启 |
|
||||
| `PASCAL_INSTALL_DIR` | Pascal 源码目录 |
|
||||
| `PASCAL_DATA_DIR` | SQLite 数据目录 |
|
||||
| `PASCAL_GIT_REPO` | Pascal 分支所在 Git 仓库;默认使用主站 origin |
|
||||
| `PASCAL_GIT_REF` | Pascal 分支或标签 |
|
||||
| `PASCAL_PORT` | Pascal 本地监听端口 |
|
||||
| `PASCAL_BASE_PATH` | 同域挂载路径 |
|
||||
| `PASCAL_PUBLIC_ORIGIN` | 公网主站 Origin,不带末尾斜杠 |
|
||||
| `NGINX_SITE_CONFIG` | 需要加入 Pascal include 的 HTTPS server 配置 |
|
||||
| `INSTALL_NGINX=false` | 仅部署服务,不修改 Nginx |
|
||||
| `PASCAL_BUN_VERSION` | 固定 Bun 版本 |
|
||||
| `PASCAL_BACKUP_DIR` | 环境与 Nginx 配置备份目录 |
|
||||
|
||||
## 运行维护
|
||||
|
||||
```bash
|
||||
pm2 status
|
||||
pm2 logs pascal-editor --lines 100
|
||||
curl http://127.0.0.1:3002/pascal-service/api/health
|
||||
nginx -t
|
||||
```
|
||||
|
||||
升级时不要把主站源码复制到 Pascal 目录,也不要把本地测试数据库上传到
|
||||
服务器。主站始终更新 `main`,Pascal 始终更新独立分支,用户场景只保存在
|
||||
`PASCAL_DATA_DIR`。
|
||||
@ -11,6 +11,9 @@
|
||||
"scripts": {
|
||||
"dev": "next dev --turbo",
|
||||
"pascal:dev": "node scripts/pascal-editor-dev.mjs",
|
||||
"pascal:deploy": "bash scripts/deploy-pascal-production.sh deploy",
|
||||
"pascal:check": "bash scripts/deploy-pascal-production.sh check",
|
||||
"pascal:smoke": "bash scripts/smoke-pascal-agent.sh",
|
||||
"postinstall": "node scripts/security/auto-enable-perf-guard.mjs",
|
||||
"prebuild": "node scripts/security/auto-enable-perf-guard.mjs",
|
||||
"build": "next build",
|
||||
|
||||
391
scripts/deploy-pascal-production.sh
Executable file
391
scripts/deploy-pascal-production.sh
Executable file
@ -0,0 +1,391 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
MODE="${1:-deploy}"
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
MAIN_APP_DIR="${MAIN_APP_DIR:-$ROOT_DIR}"
|
||||
MAIN_ENV_FILE="${MAIN_ENV_FILE:-$MAIN_APP_DIR/.env.local}"
|
||||
MAIN_PM2_NAME="${MAIN_PM2_NAME:-ai-chatbot}"
|
||||
MAIN_INTERNAL_URL="${MAIN_INTERNAL_URL:-http://127.0.0.1:3125}"
|
||||
MAIN_NGINX_UPSTREAM="${MAIN_NGINX_UPSTREAM:-127.0.0.1:3125}"
|
||||
BUILD_MAIN_APP="${BUILD_MAIN_APP:-true}"
|
||||
|
||||
PASCAL_INSTALL_DIR="${PASCAL_INSTALL_DIR:-/opt/pascal-editor}"
|
||||
PASCAL_DATA_DIR="${PASCAL_DATA_DIR:-/var/lib/pascal-editor}"
|
||||
PASCAL_GIT_REF="${PASCAL_GIT_REF:-pascal-editor-ai-agent}"
|
||||
PASCAL_GIT_REPO="${PASCAL_GIT_REPO:-}"
|
||||
PASCAL_PM2_NAME="${PASCAL_PM2_NAME:-pascal-editor}"
|
||||
PASCAL_PORT="${PASCAL_PORT:-3002}"
|
||||
PASCAL_NGINX_UPSTREAM="${PASCAL_NGINX_UPSTREAM:-127.0.0.1:${PASCAL_PORT}}"
|
||||
PASCAL_BASE_PATH="${PASCAL_BASE_PATH:-/pascal-service}"
|
||||
PASCAL_PUBLIC_ORIGIN="${PASCAL_PUBLIC_ORIGIN:-}"
|
||||
PASCAL_BUN_VERSION="${PASCAL_BUN_VERSION:-1.3.0}"
|
||||
PASCAL_BUN_BIN="${PASCAL_BUN_BIN:-$HOME/.bun/bin/bun}"
|
||||
|
||||
NGINX_SITE_CONFIG="${NGINX_SITE_CONFIG:-}"
|
||||
NGINX_SNIPPET_PATH="${NGINX_SNIPPET_PATH:-/etc/nginx/snippets/pascal-service.conf}"
|
||||
INSTALL_NGINX="${INSTALL_NGINX:-true}"
|
||||
RUN_AGENT_SMOKE_TEST="${RUN_AGENT_SMOKE_TEST:-false}"
|
||||
PASCAL_BACKUP_DIR="${PASCAL_BACKUP_DIR:-/var/backups/pascal-deploy}"
|
||||
|
||||
log() {
|
||||
printf '[pascal-deploy] %s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[pascal-deploy] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
is_true() {
|
||||
case "$1" in
|
||||
1 | true | TRUE | True | yes | YES | Yes | on | ON | On) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
|
||||
}
|
||||
|
||||
read_env_value() {
|
||||
local key="$1"
|
||||
local file="$2"
|
||||
[ -f "$file" ] || return 0
|
||||
sed -n "s/^${key}=//p" "$file" | tail -n 1
|
||||
}
|
||||
|
||||
upsert_env_value() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local file="$3"
|
||||
local temp
|
||||
mkdir -p "$(dirname "$file")"
|
||||
touch "$file"
|
||||
temp="$(mktemp)"
|
||||
awk -v prefix="${key}=" 'index($0, prefix) != 1 { print }' "$file" >"$temp"
|
||||
printf '%s=%s\n' "$key" "$value" >>"$temp"
|
||||
mv "$temp" "$file"
|
||||
chmod 600 "$file"
|
||||
}
|
||||
|
||||
generate_secret() {
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
openssl rand -hex 32
|
||||
return
|
||||
fi
|
||||
od -An -N32 -tx1 /dev/urandom | tr -d ' \n'
|
||||
}
|
||||
|
||||
resolve_secret() {
|
||||
local configured="${PASCAL_PROXY_SECRET:-}"
|
||||
if [ -z "$configured" ]; then
|
||||
configured="$(read_env_value PASCAL_PROXY_SECRET "$MAIN_ENV_FILE")"
|
||||
fi
|
||||
if [ -z "$configured" ] && [ "$MODE" = "deploy" ]; then
|
||||
configured="$(generate_secret)"
|
||||
fi
|
||||
[[ "$configured" =~ ^[A-Za-z0-9_-]{32,128}$ ]] ||
|
||||
die "PASCAL_PROXY_SECRET must contain 32-128 letters, numbers, underscores, or hyphens"
|
||||
printf '%s' "$configured"
|
||||
}
|
||||
|
||||
normalize_base_path() {
|
||||
local value="${1#/}"
|
||||
value="${value%/}"
|
||||
[ -n "$value" ] || die "PASCAL_BASE_PATH cannot be empty"
|
||||
printf '/%s' "$value"
|
||||
}
|
||||
|
||||
resolve_public_host() {
|
||||
local value="${PASCAL_PUBLIC_ORIGIN#*://}"
|
||||
value="${value%%/*}"
|
||||
value="${value%%:*}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
install_bun() {
|
||||
if [ -x "$PASCAL_BUN_BIN" ] &&
|
||||
[ "$($PASCAL_BUN_BIN --version 2>/dev/null || true)" = "$PASCAL_BUN_VERSION" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "Installing Bun requires root; install Bun $PASCAL_BUN_VERSION manually"
|
||||
require_command curl
|
||||
if ! command -v unzip >/dev/null 2>&1; then
|
||||
require_command apt-get
|
||||
apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq unzip
|
||||
fi
|
||||
|
||||
local installer
|
||||
installer="$(mktemp)"
|
||||
curl -fsSL https://bun.sh/install -o "$installer"
|
||||
bash "$installer" "bun-v${PASCAL_BUN_VERSION}"
|
||||
rm -f "$installer"
|
||||
[ -x "$PASCAL_BUN_BIN" ] || die "Bun installation did not create $PASCAL_BUN_BIN"
|
||||
}
|
||||
|
||||
configure_main_app() {
|
||||
local secret="$1"
|
||||
local next_env_backup=""
|
||||
mkdir -p "$PASCAL_BACKUP_DIR"
|
||||
if [ -f "$MAIN_ENV_FILE" ]; then
|
||||
cp "$MAIN_ENV_FILE" "$PASCAL_BACKUP_DIR/main-env-$(date +%Y%m%d%H%M%S).bak"
|
||||
chmod 600 "$PASCAL_BACKUP_DIR"/main-env-*.bak
|
||||
fi
|
||||
upsert_env_value PASCAL_EDITOR_URL "${PASCAL_BASE_PATH}/scenes" "$MAIN_ENV_FILE"
|
||||
upsert_env_value PASCAL_PROXY_SECRET "$secret" "$MAIN_ENV_FILE"
|
||||
|
||||
if ! is_true "$BUILD_MAIN_APP"; then
|
||||
log "Skipping main application build (BUILD_MAIN_APP=$BUILD_MAIN_APP)"
|
||||
return
|
||||
fi
|
||||
|
||||
require_command pnpm
|
||||
require_command pm2
|
||||
log "Building main application in $MAIN_APP_DIR"
|
||||
if [ -f "$MAIN_APP_DIR/next-env.d.ts" ]; then
|
||||
next_env_backup="$(mktemp)"
|
||||
cp "$MAIN_APP_DIR/next-env.d.ts" "$next_env_backup"
|
||||
fi
|
||||
local build_status=0
|
||||
set +e
|
||||
(
|
||||
cd "$MAIN_APP_DIR"
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$MAIN_ENV_FILE"
|
||||
set +a
|
||||
pnpm install --frozen-lockfile
|
||||
DISABLE_PERF_GUARD_AUTO_INSTALL=1 pnpm build
|
||||
) || build_status=$?
|
||||
set -e
|
||||
if [ -n "$next_env_backup" ]; then
|
||||
cp "$next_env_backup" "$MAIN_APP_DIR/next-env.d.ts"
|
||||
rm -f "$next_env_backup"
|
||||
fi
|
||||
[ "$build_status" -eq 0 ] || die "Main application build failed"
|
||||
(
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$MAIN_ENV_FILE"
|
||||
set +a
|
||||
pm2 restart "$MAIN_PM2_NAME" --update-env
|
||||
)
|
||||
}
|
||||
|
||||
resolve_pascal_repo() {
|
||||
if [ -n "$PASCAL_GIT_REPO" ]; then
|
||||
printf '%s' "$PASCAL_GIT_REPO"
|
||||
return
|
||||
fi
|
||||
git -C "$MAIN_APP_DIR" remote get-url origin
|
||||
}
|
||||
|
||||
checkout_pascal() {
|
||||
local repo="$1"
|
||||
if [ -d "$PASCAL_INSTALL_DIR/.git" ]; then
|
||||
[ -z "$(git -C "$PASCAL_INSTALL_DIR" status --porcelain)" ] ||
|
||||
die "$PASCAL_INSTALL_DIR has local Git changes; refusing to overwrite them"
|
||||
git -C "$PASCAL_INSTALL_DIR" fetch origin "$PASCAL_GIT_REF"
|
||||
git -C "$PASCAL_INSTALL_DIR" checkout "$PASCAL_GIT_REF"
|
||||
git -C "$PASCAL_INSTALL_DIR" pull --ff-only origin "$PASCAL_GIT_REF"
|
||||
return
|
||||
fi
|
||||
|
||||
[ ! -e "$PASCAL_INSTALL_DIR" ] ||
|
||||
die "$PASCAL_INSTALL_DIR exists but is not a Git checkout"
|
||||
mkdir -p "$(dirname "$PASCAL_INSTALL_DIR")"
|
||||
git clone --single-branch --branch "$PASCAL_GIT_REF" "$repo" "$PASCAL_INSTALL_DIR"
|
||||
}
|
||||
|
||||
write_pascal_env() {
|
||||
local secret="$1"
|
||||
local env_file="$PASCAL_INSTALL_DIR/.env.production"
|
||||
umask 077
|
||||
{
|
||||
printf 'NODE_ENV=production\n'
|
||||
printf 'NEXT_PUBLIC_PASCAL_BASE_PATH=%s\n' "$PASCAL_BASE_PATH"
|
||||
printf 'PASCAL_REQUIRE_HOST_USER=true\n'
|
||||
printf 'PASCAL_DATA_DIR=%s\n' "$PASCAL_DATA_DIR"
|
||||
printf 'PASCAL_SCENE_API_TOKEN=%s\n' "$secret"
|
||||
printf 'PASCAL_AGENT_PROXY_SECRET=%s\n' "$secret"
|
||||
printf 'PASCAL_AGENT_API_KEY=%s\n' "$secret"
|
||||
printf 'PASCAL_AGENT_BASE_URL=%s/api/internal/pascal-agent/v1\n' "$MAIN_INTERNAL_URL"
|
||||
printf 'PASCAL_AGENT_MODEL=main-agent\n'
|
||||
printf 'PASCAL_AGENT_MAX_STEPS=%s\n' "${PASCAL_AGENT_MAX_STEPS:-18}"
|
||||
printf 'PASCAL_AGENT_STEP_TIMEOUT_MS=%s\n' "${PASCAL_AGENT_STEP_TIMEOUT_MS:-90000}"
|
||||
printf 'PASCAL_AGENT_TOTAL_TIMEOUT_MS=%s\n' "${PASCAL_AGENT_TOTAL_TIMEOUT_MS:-300000}"
|
||||
} >"$env_file"
|
||||
chmod 600 "$env_file"
|
||||
}
|
||||
|
||||
build_pascal() {
|
||||
mkdir -p "$PASCAL_DATA_DIR"
|
||||
chmod 700 "$PASCAL_DATA_DIR"
|
||||
export PATH="$(dirname "$PASCAL_BUN_BIN"):$PATH"
|
||||
log "Installing and building Pascal from $PASCAL_GIT_REF"
|
||||
(
|
||||
cd "$PASCAL_INSTALL_DIR"
|
||||
"$PASCAL_BUN_BIN" install --frozen-lockfile
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source .env.production
|
||||
set +a
|
||||
"$PASCAL_BUN_BIN" x turbo run build --filter=editor --output-logs=errors-only
|
||||
)
|
||||
}
|
||||
|
||||
start_pascal() {
|
||||
require_command pm2
|
||||
if pm2 describe "$PASCAL_PM2_NAME" >/dev/null 2>&1; then
|
||||
pm2 restart "$PASCAL_PM2_NAME"
|
||||
else
|
||||
pm2 start /usr/bin/bash \
|
||||
--name "$PASCAL_PM2_NAME" \
|
||||
--cwd "$PASCAL_INSTALL_DIR" \
|
||||
-- -lc "set -a; source '$PASCAL_INSTALL_DIR/.env.production'; set +a; export PATH='$(dirname "$PASCAL_BUN_BIN")':\$PATH; exec '$PASCAL_BUN_BIN' run --cwd apps/editor start -- -p '$PASCAL_PORT' -H 127.0.0.1"
|
||||
fi
|
||||
pm2 save
|
||||
}
|
||||
|
||||
render_nginx() {
|
||||
local secret="$1"
|
||||
local public_host
|
||||
public_host="$(resolve_public_host)"
|
||||
|
||||
if ! is_true "$INSTALL_NGINX"; then
|
||||
log "Skipping Nginx setup (INSTALL_NGINX=$INSTALL_NGINX)"
|
||||
return
|
||||
fi
|
||||
[ -n "$public_host" ] || die "PASCAL_PUBLIC_ORIGIN is required when INSTALL_NGINX=true"
|
||||
|
||||
if [ -z "$NGINX_SITE_CONFIG" ]; then
|
||||
NGINX_SITE_CONFIG="/etc/nginx/sites-enabled/${public_host}.conf"
|
||||
fi
|
||||
[ -f "$NGINX_SITE_CONFIG" ] || die "Nginx site config not found: $NGINX_SITE_CONFIG"
|
||||
require_command nginx
|
||||
|
||||
local template="$MAIN_APP_DIR/deploy/nginx/pascal-service.conf.template"
|
||||
local site_backup snippet_backup rendered updated_site
|
||||
[ -f "$template" ] || die "Nginx template not found: $template"
|
||||
site_backup="$(mktemp)"
|
||||
snippet_backup="$(mktemp)"
|
||||
rendered="$(mktemp)"
|
||||
updated_site="$(mktemp)"
|
||||
cp "$NGINX_SITE_CONFIG" "$site_backup"
|
||||
mkdir -p "$PASCAL_BACKUP_DIR"
|
||||
cp "$NGINX_SITE_CONFIG" "$PASCAL_BACKUP_DIR/nginx-site-$(date +%Y%m%d%H%M%S).bak"
|
||||
if [ -f "$NGINX_SNIPPET_PATH" ]; then
|
||||
cp "$NGINX_SNIPPET_PATH" "$snippet_backup"
|
||||
cp "$NGINX_SNIPPET_PATH" "$PASCAL_BACKUP_DIR/nginx-snippet-$(date +%Y%m%d%H%M%S).bak"
|
||||
else
|
||||
: >"$snippet_backup"
|
||||
fi
|
||||
|
||||
sed \
|
||||
-e "s/__PASCAL_PROXY_SECRET__/${secret}/g" \
|
||||
-e "s#__MAIN_NGINX_UPSTREAM__#${MAIN_NGINX_UPSTREAM}#g" \
|
||||
-e "s#__PASCAL_NGINX_UPSTREAM__#${PASCAL_NGINX_UPSTREAM}#g" \
|
||||
-e "s#__PASCAL_BASE_PATH__#${PASCAL_BASE_PATH}#g" \
|
||||
"$template" >"$rendered"
|
||||
install -m 600 "$rendered" "$NGINX_SNIPPET_PATH"
|
||||
|
||||
if ! grep -qF "include $NGINX_SNIPPET_PATH;" "$NGINX_SITE_CONFIG"; then
|
||||
awk -v target="server_name ${public_host};" -v include_line=" include ${NGINX_SNIPPET_PATH};" '
|
||||
!inserted && index($0, target) > 0 { print; print include_line; inserted = 1; next }
|
||||
{ print }
|
||||
END { if (!inserted) exit 42 }
|
||||
' "$NGINX_SITE_CONFIG" >"$updated_site" || {
|
||||
cp "$site_backup" "$NGINX_SITE_CONFIG"
|
||||
die "Could not find server_name ${public_host}; in $NGINX_SITE_CONFIG"
|
||||
}
|
||||
install -m 644 "$updated_site" "$NGINX_SITE_CONFIG"
|
||||
fi
|
||||
|
||||
if ! nginx -t; then
|
||||
cp "$site_backup" "$NGINX_SITE_CONFIG"
|
||||
if [ -s "$snippet_backup" ]; then
|
||||
cp "$snippet_backup" "$NGINX_SNIPPET_PATH"
|
||||
else
|
||||
rm -f "$NGINX_SNIPPET_PATH"
|
||||
fi
|
||||
nginx -t || true
|
||||
die "Nginx validation failed; previous configuration restored"
|
||||
fi
|
||||
systemctl reload nginx
|
||||
rm -f "$site_backup" "$snippet_backup" "$rendered" "$updated_site"
|
||||
}
|
||||
|
||||
check_deployment() {
|
||||
local secret="$1"
|
||||
local local_pascal="http://127.0.0.1:${PASCAL_PORT}${PASCAL_BASE_PATH}"
|
||||
log "Checking main application"
|
||||
curl -fsS "$MAIN_INTERNAL_URL/ping" >/dev/null
|
||||
log "Checking Pascal health and Agent configuration"
|
||||
curl -fsS "$local_pascal/api/health" >/dev/null
|
||||
curl -fsS \
|
||||
-H "X-Pascal-Agent-Proxy-Secret: $secret" \
|
||||
-H "X-Pascal-User-Id: deployment-health-check" \
|
||||
"$local_pascal/api/agent/chat" >/dev/null
|
||||
log "Checking the main model gateway"
|
||||
curl -fsS \
|
||||
-H "Authorization: Bearer $secret" \
|
||||
"$MAIN_INTERNAL_URL/api/internal/pascal-agent/v1/chat/completions"
|
||||
printf '\n'
|
||||
|
||||
if [ -n "$PASCAL_PUBLIC_ORIGIN" ]; then
|
||||
local public_code
|
||||
public_code="$(curl -sS -o /dev/null -w '%{http_code}' "${PASCAL_PUBLIC_ORIGIN}${PASCAL_BASE_PATH}/scenes")"
|
||||
case "$public_code" in
|
||||
200 | 302 | 307 | 401) ;;
|
||||
*) die "Unexpected public Pascal status: $public_code" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
log "Deployment checks passed"
|
||||
}
|
||||
|
||||
main() {
|
||||
case "$MODE" in
|
||||
deploy | check) ;;
|
||||
*) die "Usage: $0 [deploy|check]" ;;
|
||||
esac
|
||||
|
||||
require_command curl
|
||||
require_command git
|
||||
require_command sed
|
||||
PASCAL_BASE_PATH="$(normalize_base_path "$PASCAL_BASE_PATH")"
|
||||
local secret
|
||||
secret="$(resolve_secret)"
|
||||
|
||||
if [ "$MODE" = "check" ]; then
|
||||
check_deployment "$secret"
|
||||
return
|
||||
fi
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "Production deployment must run as root"
|
||||
configure_main_app "$secret"
|
||||
install_bun
|
||||
checkout_pascal "$(resolve_pascal_repo)"
|
||||
write_pascal_env "$secret"
|
||||
build_pascal
|
||||
start_pascal
|
||||
render_nginx "$secret"
|
||||
sleep 3
|
||||
check_deployment "$secret"
|
||||
|
||||
if is_true "$RUN_AGENT_SMOKE_TEST"; then
|
||||
PASCAL_SMOKE_CONFIRM=1 \
|
||||
PASCAL_ENV_FILE="$PASCAL_INSTALL_DIR/.env.production" \
|
||||
PASCAL_PORT="$PASCAL_PORT" \
|
||||
PASCAL_BASE_PATH="$PASCAL_BASE_PATH" \
|
||||
"$MAIN_APP_DIR/scripts/smoke-pascal-agent.sh"
|
||||
fi
|
||||
|
||||
log "Pascal is ready at ${PASCAL_PUBLIC_ORIGIN:-<public-origin>}/pascal-editor"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
100
scripts/smoke-pascal-agent.sh
Executable file
100
scripts/smoke-pascal-agent.sh
Executable file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
if [ "${PASCAL_SMOKE_CONFIRM:-}" != "1" ]; then
|
||||
printf '%s\n' \
|
||||
'This check makes one real model request and then deletes its temporary scene.' \
|
||||
'Run with PASCAL_SMOKE_CONFIRM=1 after confirming provider usage is acceptable.' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
command -v curl >/dev/null 2>&1 || { printf 'curl is required.\n' >&2; exit 1; }
|
||||
command -v node >/dev/null 2>&1 || { printf 'node is required.\n' >&2; exit 1; }
|
||||
|
||||
PASCAL_ENV_FILE="${PASCAL_ENV_FILE:-/opt/pascal-editor/.env.production}"
|
||||
PASCAL_PORT="${PASCAL_PORT:-3002}"
|
||||
PASCAL_BASE_PATH="/${PASCAL_BASE_PATH#/}"
|
||||
PASCAL_BASE_PATH="${PASCAL_BASE_PATH%/}"
|
||||
BASE_URL="${PASCAL_INTERNAL_URL:-http://127.0.0.1:${PASCAL_PORT}}${PASCAL_BASE_PATH}"
|
||||
USER_ID="deployment-smoke-test"
|
||||
SCENE_ID="deployment-smoke-scene"
|
||||
|
||||
read_env_value() {
|
||||
sed -n "s/^${1}=//p" "$PASCAL_ENV_FILE" | tail -n 1
|
||||
}
|
||||
|
||||
SECRET="${PASCAL_PROXY_SECRET:-$(read_env_value PASCAL_AGENT_PROXY_SECRET)}"
|
||||
[ -n "$SECRET" ] || {
|
||||
printf 'PASCAL proxy secret is not configured.\n' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stream_file="$(mktemp)"
|
||||
scene_file="$(mktemp)"
|
||||
prompt="${PASCAL_SMOKE_PROMPT:-创建一个4米乘4米的简单房间,添加一扇门和一扇窗,检查结构后保存。}"
|
||||
|
||||
cleanup() {
|
||||
curl -sS -o /dev/null -X DELETE \
|
||||
-H "X-Pascal-Scene-Token: $SECRET" \
|
||||
-H "X-Pascal-User-Id: $USER_ID" \
|
||||
"$BASE_URL/api/scenes/$SCENE_ID" || true
|
||||
rm -f "$stream_file" "$scene_file"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cleanup
|
||||
|
||||
payload="$(node -e '
|
||||
const sceneId = process.argv[1];
|
||||
const prompt = process.argv[2];
|
||||
process.stdout.write(JSON.stringify({
|
||||
sceneId,
|
||||
messages: [{
|
||||
id: "deployment-smoke-message",
|
||||
role: "user",
|
||||
parts: [{ type: "text", text: prompt }],
|
||||
}],
|
||||
}));
|
||||
' "$SCENE_ID" "$prompt")"
|
||||
|
||||
curl -fsS -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H "X-Pascal-Scene-Token: $SECRET" \
|
||||
-H "X-Pascal-User-Id: $USER_ID" \
|
||||
--data "{\"id\":\"$SCENE_ID\",\"name\":\"Deployment smoke test\",\"graph\":{\"nodes\":{},\"rootNodeIds\":[]}}" \
|
||||
"$BASE_URL/api/scenes" >/dev/null
|
||||
|
||||
http_code="$(curl -sS --max-time "${PASCAL_SMOKE_TIMEOUT_SECONDS:-360}" \
|
||||
-o "$stream_file" \
|
||||
-w '%{http_code}' \
|
||||
-X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H "X-Pascal-Agent-Proxy-Secret: $SECRET" \
|
||||
-H "X-Pascal-User-Id: $USER_ID" \
|
||||
--data "$payload" \
|
||||
"$BASE_URL/api/agent/chat")"
|
||||
|
||||
[ "$http_code" = "200" ] || {
|
||||
printf 'Agent smoke request returned HTTP %s.\n' "$http_code" >&2
|
||||
exit 1
|
||||
}
|
||||
if grep -q '"type":"error"' "$stream_file"; then
|
||||
printf 'Agent smoke stream reported an error.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -fsS \
|
||||
-H "X-Pascal-Scene-Token: $SECRET" \
|
||||
-H "X-Pascal-User-Id: $USER_ID" \
|
||||
"$BASE_URL/api/scenes/$SCENE_ID" >"$scene_file"
|
||||
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const scene = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||
const expectedOwner = process.argv[2];
|
||||
if (!(scene.nodeCount > 0)) throw new Error("Agent did not persist scene nodes");
|
||||
if (scene.ownerId !== expectedOwner) throw new Error("Scene owner mismatch");
|
||||
console.log(JSON.stringify({ nodeCount: scene.nodeCount, ownerId: scene.ownerId, version: scene.version }));
|
||||
' "$scene_file" "$USER_ID"
|
||||
|
||||
printf '[pascal-smoke] Real model and MCP persistence check passed.\n'
|
||||
Loading…
x
Reference in New Issue
Block a user