- Fix deploy_crm_only.sh: non-destructive hot reload - Enhanced stop logic with pgrep + fuser for port release - Added 3-layer verification (process/control/user) - Check /proc/$pid/exe for (deleted) marker - Never delete DB - Fix portal script contracts: crm_session → crm_subject - deploy_tksea_portal.sh: use $cookie_crm_subject - test_tksea_portal_assets.sh: assert crm_subject exists - nginx.example.conf: updated trusted subject header - Add systemd service management - sub2api-crm.service.template - install_crm_systemd.sh - verify_crm_deployment.sh Update docs/plans/2026-06-04-next-version-plan.md with deployment findings.
213 lines
7.2 KiB
Bash
Executable File
213 lines
7.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||
DEPLOY_ENV_FILE="${DEPLOY_ENV_FILE:-$ROOT_DIR/scripts/deploy/.env.deploy}"
|
||
if [[ -f "$DEPLOY_ENV_FILE" ]]; then
|
||
set -a
|
||
# shellcheck disable=SC1090
|
||
source "$DEPLOY_ENV_FILE"
|
||
set +a
|
||
fi
|
||
|
||
KEY="${KEY:-}"
|
||
REMOTE="${REMOTE:-}"
|
||
REMOTE_PORTAL_DIR="${REMOTE_PORTAL_DIR:-/var/www/sub2api-portal}"
|
||
REMOTE_NGINX_SITE="${REMOTE_NGINX_SITE:-/etc/nginx/sites-available/tksea}"
|
||
REMOTE_HOST_PORT="${REMOTE_HOST_PORT:-8080}"
|
||
REMOTE_CRM_PORT="${REMOTE_CRM_PORT:-}"
|
||
LOCAL_PORTAL_DIR="${LOCAL_PORTAL_DIR:-$ROOT_DIR/deploy/tksea-portal}"
|
||
REMOTE_STAGE_DIR="${REMOTE_STAGE_DIR:-/tmp/sub2api-portal-deploy}"
|
||
DRY_RUN="${DRY_RUN:-0}"
|
||
|
||
die() {
|
||
echo "$*" >&2
|
||
exit 1
|
||
}
|
||
|
||
require_cmd() {
|
||
command -v "$1" >/dev/null 2>&1 || die "missing command: $1"
|
||
}
|
||
|
||
run_cmd() {
|
||
if [[ "$DRY_RUN" == "1" ]]; then
|
||
printf 'DRY_RUN:'
|
||
printf ' %q' "$@"
|
||
printf '\n'
|
||
return 0
|
||
fi
|
||
"$@"
|
||
}
|
||
|
||
ssh_remote() {
|
||
run_cmd ssh -i "$KEY" -o StrictHostKeyChecking=no "$REMOTE" "$@"
|
||
}
|
||
|
||
scp_remote() {
|
||
run_cmd scp -i "$KEY" -o StrictHostKeyChecking=no "$@"
|
||
}
|
||
|
||
main() {
|
||
require_cmd python3
|
||
require_cmd ssh
|
||
require_cmd scp
|
||
|
||
[[ -d "$LOCAL_PORTAL_DIR" ]] || die "missing portal dir: $LOCAL_PORTAL_DIR"
|
||
[[ -f "$LOCAL_PORTAL_DIR/index.html" ]] || die "missing portal index: $LOCAL_PORTAL_DIR/index.html"
|
||
[[ -n "$KEY" ]] || die "KEY is required; copy scripts/deploy/.env.deploy.example to scripts/deploy/.env.deploy and fill it"
|
||
[[ -n "$REMOTE" ]] || die "REMOTE is required; copy scripts/deploy/.env.deploy.example to scripts/deploy/.env.deploy and fill it"
|
||
[[ -n "$REMOTE_CRM_PORT" ]] || die "REMOTE_CRM_PORT is required; copy scripts/deploy/.env.deploy.example to scripts/deploy/.env.deploy and fill it"
|
||
if [[ "$DRY_RUN" != "1" ]]; then
|
||
[[ -f "$KEY" ]] || die "missing ssh key: $KEY"
|
||
fi
|
||
|
||
local tmpdir patch_file portal_stage_dir
|
||
tmpdir="$(mktemp -d)"
|
||
trap "rm -rf $(printf '%q' "$tmpdir")" EXIT
|
||
patch_file="$tmpdir/patch_tksea_portal_nginx.py"
|
||
portal_stage_dir="$tmpdir/portal"
|
||
|
||
mkdir -p "$portal_stage_dir"
|
||
cp -R "$LOCAL_PORTAL_DIR/." "$portal_stage_dir/"
|
||
|
||
cat > "$patch_file" <<EOF
|
||
from pathlib import Path
|
||
import re
|
||
import textwrap
|
||
|
||
|
||
path = Path(${REMOTE_NGINX_SITE@Q})
|
||
text = path.read_text()
|
||
block = textwrap.dedent("""\
|
||
location = /portal {
|
||
return 302 /portal/;
|
||
}
|
||
|
||
location = /portal/admin {
|
||
return 302 /portal/admin/;
|
||
}
|
||
|
||
location = /kimi-portal {
|
||
return 302 /portal/;
|
||
}
|
||
|
||
# BEGIN sub2api-portal
|
||
location /portal/ {
|
||
alias ${REMOTE_PORTAL_DIR}/;
|
||
index index.html;
|
||
try_files \$uri \$uri/ /portal/index.html;
|
||
}
|
||
|
||
location /portal-proxy/ {
|
||
proxy_pass http://127.0.0.1:${REMOTE_HOST_PORT}/;
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
proxy_http_version 1.1;
|
||
}
|
||
|
||
location /portal-admin-api/ {
|
||
# 必须由受信登录/鉴权层把用户签名放进 $cookie_crm_subject,不能信任浏览器自带 header。
|
||
# 这是 CRM 配置 TRUSTED_SUBJECT_COOKIE=crm_subject 对应的 cookie 名。
|
||
# 同时 CRM 需配置:
|
||
# SUB2API_CRM_TRUSTED_SUBJECT_HEADER=X-CRM-Authenticated-Subject
|
||
# SUB2API_CRM_TRUSTED_PROXY_SECRET_HEADER=X-CRM-Trusted-Proxy
|
||
# SUB2API_CRM_TRUSTED_PROXY_SECRET=<same-secret-as-nginx>
|
||
proxy_pass http://127.0.0.1:${REMOTE_CRM_PORT}/;
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
proxy_set_header X-CRM-Authenticated-Subject \$cookie_crm_subject;
|
||
proxy_set_header X-CRM-Trusted-Proxy "REPLACE_WITH_SUB2API_CRM_TRUSTED_PROXY_SECRET";
|
||
proxy_http_version 1.1;
|
||
}
|
||
|
||
location = /v1/chat/completions {
|
||
proxy_pass http://127.0.0.1:${REMOTE_CRM_PORT}/v1/chat/completions;
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
proxy_http_version 1.1;
|
||
}
|
||
|
||
location /kimi-portal/ {
|
||
return 302 /portal/;
|
||
}
|
||
|
||
location /kimi-portal-proxy/ {
|
||
proxy_pass http://127.0.0.1:${REMOTE_HOST_PORT}/;
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
proxy_http_version 1.1;
|
||
}
|
||
|
||
location /kimi/ {
|
||
proxy_pass http://127.0.0.1:${REMOTE_HOST_PORT}/;
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
proxy_http_version 1.1;
|
||
}
|
||
|
||
location /kimi-v1/ {
|
||
proxy_pass http://127.0.0.1:${REMOTE_HOST_PORT}/v1/;
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
proxy_http_version 1.1;
|
||
}
|
||
# END sub2api-portal
|
||
|
||
""")
|
||
|
||
patterns = [
|
||
re.compile(r"\\n\\s*location = /portal \\{.*?# END sub2api-portal\\n\\n", re.S),
|
||
re.compile(r"\\n\\s*location = /kimi-portal \\{.*?# END kimi-portal\\n\\n", re.S),
|
||
]
|
||
|
||
for pattern in patterns:
|
||
if pattern.search(text):
|
||
text = pattern.sub("\\n" + block + "\\n", text, count=1)
|
||
path.write_text(text)
|
||
raise SystemExit(0)
|
||
|
||
needle = "\\n location / {\\n proxy_pass http://127.0.0.1:"
|
||
index = text.rfind(needle)
|
||
if index == -1:
|
||
raise SystemExit("failed to locate sub.tksea.top root location block")
|
||
|
||
text = text[:index] + "\\n" + block + text[index:]
|
||
path.write_text(text)
|
||
EOF
|
||
|
||
ssh_remote "mkdir -p $(printf '%q' "$REMOTE_STAGE_DIR")"
|
||
scp_remote -r "$portal_stage_dir" "$REMOTE:$REMOTE_STAGE_DIR/"
|
||
scp_remote "$patch_file" "$REMOTE:$REMOTE_STAGE_DIR/patch_tksea_portal_nginx.py"
|
||
ssh_remote "sudo install -d -m 755 $(printf '%q' "$REMOTE_PORTAL_DIR") && sudo cp -R $(printf '%q' "$REMOTE_STAGE_DIR/portal/.") $(printf '%q' "$REMOTE_PORTAL_DIR/") && sudo python3 $(printf '%q' "$REMOTE_STAGE_DIR/patch_tksea_portal_nginx.py") && sudo nginx -t && sudo systemctl reload nginx"
|
||
|
||
cat <<EOF
|
||
tksea portal deployed
|
||
remote: ${REMOTE}
|
||
portal url: https://sub.tksea.top/portal/
|
||
portal admin home url: https://sub.tksea.top/portal/admin/
|
||
logical groups admin url: https://sub.tksea.top/portal/admin/logical-groups.html
|
||
route health admin url: https://sub.tksea.top/portal/admin/route-health.html
|
||
accounts admin url: https://sub.tksea.top/portal/admin/accounts.html
|
||
provider admin url: https://sub.tksea.top/portal/admin/providers.html
|
||
batch import admin url: https://sub.tksea.top/portal/admin/batch-import.html
|
||
batch import admin url: https://sub.tksea.top/portal/admin-batch-import.html
|
||
legacy url: https://sub.tksea.top/kimi-portal/
|
||
portal dir: ${REMOTE_PORTAL_DIR}
|
||
nginx site: ${REMOTE_NGINX_SITE}
|
||
EOF
|
||
}
|
||
|
||
main "$@"
|