chore: checkpoint admin restore state
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
.git
|
||||
.gitignore
|
||||
.DS_Store
|
||||
.env
|
||||
.env.example
|
||||
.env.production
|
||||
.adm-api.pid
|
||||
.adm-dev.pid
|
||||
.adm-preview.pid
|
||||
.claude
|
||||
.mindops
|
||||
node_modules
|
||||
coverage
|
||||
*.log
|
||||
adm-preview.log
|
||||
adm-api.log
|
||||
# 开发源码与文档 — 103 只允许运行产物,不允许保留可编辑源码树
|
||||
src
|
||||
docs
|
||||
tsconfig.app.json
|
||||
tsconfig.json
|
||||
tsconfig.node.json
|
||||
vite.config.ts
|
||||
# 开发入口页(dist/index.html 必须保留)
|
||||
/index.html
|
||||
DEVELOPMENT_RELEASE_RULES.md
|
||||
ENGINEERING_WORKFLOW_RULES.md
|
||||
PRODUCTION_RELEASE_RULES.md
|
||||
TEST_RELEASE_RULES.md
|
||||
# 仅本机开发/运维脚本
|
||||
scripts/dev.mjs
|
||||
scripts/local_restart.sh
|
||||
scripts/pro_restart.sh
|
||||
scripts/rsync_to_server.sh
|
||||
scripts/audit-103-state.sh
|
||||
scripts/gadm-nginx-105.conf
|
||||
scripts/gadm-nginx.conf.example
|
||||
scripts/gadm-lb.Caddyfile
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
HOST="${STUDIO_HOST:-john@10.10.0.2}"
|
||||
APP_DIR="${STUDIO_APP_DIR:-/Users/john/Project/memind_adm}"
|
||||
|
||||
ssh -o BatchMode=yes "${HOST}" "
|
||||
set -e
|
||||
echo '== host =='
|
||||
hostname
|
||||
echo
|
||||
echo '== app dir =='
|
||||
echo '${APP_DIR}'
|
||||
echo
|
||||
echo '== git =='
|
||||
cd '${APP_DIR}'
|
||||
git rev-parse HEAD 2>/dev/null || echo 'no-git'
|
||||
git status --short --branch 2>/dev/null | sed -n '1,120p' || true
|
||||
echo
|
||||
echo '== env target =='
|
||||
sed -n '1,120p' .env 2>/dev/null | grep -E '^(DATABASE_URL|MYSQL_|MEMIND_LIB_ROOT|H5_USERS_ROOT|PLAN_SYNC_)' || true
|
||||
echo
|
||||
echo '== health =='
|
||||
curl -s http://127.0.0.1:8085/health || true
|
||||
echo
|
||||
curl -sI http://127.0.0.1:5174/ | head -1 || true
|
||||
"
|
||||
@@ -1,4 +1,5 @@
|
||||
# gadm.tkmind.cn → 本机管理端 (vite preview :5174,API 反代 :8085)
|
||||
# gadm.tkmind.cn → memind_adm(vite preview :5174 + Admin API :8085)
|
||||
# 生产 nginx 需把 /ops/ 反代到 5174,勿再指向 Memind/ops dev server
|
||||
:8091 {
|
||||
reverse_proxy 127.0.0.1:5174 {
|
||||
flush_interval -1
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# gadm.tkmind.cn @ 105 nginx → Studio LAN 10.10.0.2(fallback 58.38.22.103)
|
||||
# memind_adm: preview :5174, Admin API :8085(根路径部署,/ 直达后台)
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name gadm.tkmind.cn;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name gadm.tkmind.cn;
|
||||
ssl_certificate /etc/letsencrypt/live/gadm.tkmind.cn/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/gadm.tkmind.cn/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
location ^~ /ops/admin {
|
||||
return 301 /;
|
||||
}
|
||||
|
||||
location /auth {
|
||||
proxy_pass http://10.10.0.2:8085;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-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_read_timeout 120;
|
||||
proxy_send_timeout 120;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location /admin-api {
|
||||
proxy_pass http://10.10.0.2:8085;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-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_read_timeout 120;
|
||||
proxy_send_timeout 120;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://10.10.0.2:8085;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-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_read_timeout 120;
|
||||
proxy_send_timeout 120;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://10.10.0.2:5174;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-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_read_timeout 120;
|
||||
proxy_send_timeout 120;
|
||||
proxy_buffering off;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
# gadm.tkmind.cn — memind_adm 独立管理端(替换原 Memind /ops Vite dev)
|
||||
#
|
||||
# 前置:100 服务器 remote_restart.sh 已启动
|
||||
# Admin API: 127.0.0.1:8085
|
||||
# vite preview: 127.0.0.1:5174(静态资源 base=/ops/)
|
||||
#
|
||||
# 前端 fetch 使用根路径 /auth、/admin-api、/api(非 /ops/auth),nginx 需单独反代到 Admin API。
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name gadm.tkmind.cn;
|
||||
|
||||
# ssl_certificate ...;
|
||||
|
||||
location = / {
|
||||
return 302 /ops/admin/;
|
||||
}
|
||||
|
||||
location /auth {
|
||||
proxy_pass http://127.0.0.1:8085;
|
||||
proxy_http_version 1.1;
|
||||
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;
|
||||
}
|
||||
|
||||
location /admin-api {
|
||||
proxy_pass http://127.0.0.1:8085;
|
||||
proxy_http_version 1.1;
|
||||
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;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:8085;
|
||||
proxy_http_version 1.1;
|
||||
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;
|
||||
}
|
||||
|
||||
location /ops/ {
|
||||
proxy_pass http://127.0.0.1:5174;
|
||||
proxy_http_version 1.1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
+37
-12
@@ -1,8 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
import crypto from 'node:crypto';
|
||||
import readline from 'node:readline/promises';
|
||||
import { stdin as input, stdout as output } from 'node:process';
|
||||
import { Algorithm as Argon2Algorithm, hashRawSync as argon2HashRawSync } from '@node-rs/argon2';
|
||||
import { createDbPool, isDatabaseConfigured } from '../server/db.mjs';
|
||||
import { createLocalUserAuth } from '../server/local-auth.mjs';
|
||||
|
||||
const PASSWORD_ALGORITHM_ARGON2ID = 'argon2id';
|
||||
const ARGON2_MEMORY = 64 * 1024;
|
||||
const ARGON2_PASSES = 3;
|
||||
const ARGON2_PARALLELISM = 1;
|
||||
const ARGON2_TAG_LENGTH = 32;
|
||||
|
||||
function createPasswordRecord(password) {
|
||||
const salt = crypto.randomBytes(16).toString('hex');
|
||||
const passwordHash = argon2HashRawSync(password, {
|
||||
salt: Buffer.from(salt, 'hex'),
|
||||
parallelism: ARGON2_PARALLELISM,
|
||||
outputLen: ARGON2_TAG_LENGTH,
|
||||
memoryCost: ARGON2_MEMORY,
|
||||
timeCost: ARGON2_PASSES,
|
||||
algorithm: Argon2Algorithm.Argon2id,
|
||||
}).toString('hex');
|
||||
return { salt, passwordHash, passwordAlgorithm: PASSWORD_ALGORITHM_ARGON2ID };
|
||||
}
|
||||
|
||||
if (!isDatabaseConfigured()) {
|
||||
console.error('MySQL 未配置,请先设置 DATABASE_URL 或 MYSQL_*');
|
||||
@@ -30,18 +50,23 @@ if (!password || password !== confirm) {
|
||||
}
|
||||
|
||||
const pool = createDbPool();
|
||||
const auth = createLocalUserAuth(pool);
|
||||
await auth.ensureAdminUser().catch(() => {});
|
||||
const [rows] = await pool.execute(
|
||||
`SELECT id FROM h5_users WHERE username = ? AND role = 'admin' LIMIT 1`,
|
||||
[username],
|
||||
);
|
||||
if (!rows.length) {
|
||||
console.error(`未找到共享主用户表中的 admin 账号: ${username}`);
|
||||
await pool.end();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const nextPassword = createPasswordRecord(password);
|
||||
await pool.execute(
|
||||
`INSERT INTO auth_users (username, display_name, role, status, password_hash, balance_cents)
|
||||
VALUES (?, ?, 'admin', 'active', ?, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
display_name = VALUES(display_name),
|
||||
role = VALUES(role),
|
||||
status = VALUES(status),
|
||||
password_hash = VALUES(password_hash)`,
|
||||
[username, username, auth.hashPassword(password)],
|
||||
`UPDATE h5_users
|
||||
SET salt = ?, password_hash = ?, password_algorithm = ?, updated_at = ?
|
||||
WHERE id = ?`,
|
||||
[nextPassword.salt, nextPassword.passwordHash, nextPassword.passwordAlgorithm, Date.now(), rows[0].id],
|
||||
);
|
||||
|
||||
console.log(`admin 账号已写入数据库: ${username}`);
|
||||
console.log(`admin 账号密码已更新到 h5_users: ${username}`);
|
||||
await pool.end();
|
||||
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PORT="${ADM_PORT:-5174}"
|
||||
API_PORT="${ADM_API_PORT:-8085}"
|
||||
NODE_PATH="${LOCAL_NODE_PATH:-/opt/homebrew/opt/node@24/bin:/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
|
||||
LOG="${ROOT}/adm-preview.log"
|
||||
API_LOG="${ROOT}/adm-api.log"
|
||||
PID_FILE="${ROOT}/.adm-preview.pid"
|
||||
API_PID_FILE="${ROOT}/.adm-api.pid"
|
||||
|
||||
export PATH="${NODE_PATH}:${PATH}"
|
||||
command -v node >/dev/null 2>&1 || { echo "❌ 未找到 node" >&2; exit 1; }
|
||||
|
||||
cd "${ROOT}"
|
||||
|
||||
if [[ ! -d node_modules ]]; then
|
||||
echo "==> 安装依赖..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
echo "==> 本机构建..."
|
||||
npm run build
|
||||
|
||||
echo "==> 停止旧进程 (port ${PORT}, ${API_PORT})..."
|
||||
lsof -ti "TCP:${PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
|
||||
lsof -ti "TCP:${API_PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
|
||||
sleep 1
|
||||
lsof -ti "TCP:${PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null || true
|
||||
lsof -ti "TCP:${API_PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null || true
|
||||
rm -f "${PID_FILE}" "${API_PID_FILE}"
|
||||
|
||||
echo "==> 启动本机 Admin API @ 127.0.0.1:${API_PORT}..."
|
||||
nohup node server/index.mjs >>"${API_LOG}" 2>&1 &
|
||||
echo $! >"${API_PID_FILE}"
|
||||
sleep 2
|
||||
|
||||
if ! curl -sf "http://127.0.0.1:${API_PORT}/health" >/dev/null; then
|
||||
echo "❌ 本机 Admin API 启动失败,最近日志:" >&2
|
||||
tail -n 60 "${API_LOG}" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> 启动本机 vite preview @ 127.0.0.1:${PORT}..."
|
||||
nohup npm run dev:preview -- --host 127.0.0.1 --port "${PORT}" >>"${LOG}" 2>&1 &
|
||||
echo $! >"${PID_FILE}"
|
||||
sleep 2
|
||||
|
||||
if ! curl -sf "http://127.0.0.1:${PORT}/" >/dev/null; then
|
||||
echo "❌ 本机 preview 启动失败,最近日志:" >&2
|
||||
tail -n 60 "${LOG}" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ local_restart 完成: http://127.0.0.1:${PORT}/"
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
cd "${ROOT}"
|
||||
|
||||
echo "==> 在当前机器执行 pro_restart"
|
||||
bash "${ROOT}/scripts/remote_restart.sh"
|
||||
|
||||
echo "✅ pro_restart 完成"
|
||||
Executable
+283
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
HOST="${STUDIO_HOST:-10.10.0.2}"
|
||||
REMOTE_ROOT="${STUDIO_REMOTE_ROOT:-/Users/john/Project}"
|
||||
APP_NAME="${STUDIO_APP_NAME:-memind_adm}"
|
||||
APP_DIR="${REMOTE_ROOT}/${APP_NAME}"
|
||||
INCOMING_DIR="${REMOTE_ROOT}/incoming/memind_adm"
|
||||
RELEASES_DIR="${REMOTE_ROOT}/releases"
|
||||
BACKUP_DIR="${REMOTE_ROOT}/backups/memind_adm"
|
||||
ARCHIVE_DIR="${REMOTE_ROOT}/archives"
|
||||
IGNORE_FILE="${ROOT}/scripts/.releaseignore-prod"
|
||||
WEB_HEALTH_URL="${STUDIO_WEB_HEALTH_URL:-http://127.0.0.1:5174/ops/}"
|
||||
API_HEALTH_URL="${STUDIO_API_HEALTH_URL:-http://127.0.0.1:8085/health}"
|
||||
RELEASE_TS="$(date +%Y%m%d-%H%M%S)"
|
||||
SHORT_SHA="$(git -C "${ROOT}" rev-parse --short HEAD 2>/dev/null || echo no-git)"
|
||||
RELEASE_ID="${RELEASE_TS}-${SHORT_SHA}"
|
||||
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/memindadm-release.XXXXXX")"
|
||||
BUNDLE_PATH="${TMP_DIR}/memind-adm-${RELEASE_ID}.tar.gz"
|
||||
MANIFEST_PATH="${TMP_DIR}/memind-adm-${RELEASE_ID}.manifest.txt"
|
||||
SHA_PATH="${TMP_DIR}/memind-adm-${RELEASE_ID}.sha256"
|
||||
MEMIND_SRC="${TEST_MEMIND_ROOT:-${ROOT}/../test-memind}"
|
||||
MEMIND_LIB="${ROOT}/memind-lib"
|
||||
DRY_RUN=0
|
||||
SKIP_BUILD=0
|
||||
AUTO_YES=0
|
||||
|
||||
cleanup() {
|
||||
rm -rf "${TMP_DIR}" "${MEMIND_LIB}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
bash scripts/release-prod.sh [--dry-run] [--skip-build] [--yes]
|
||||
|
||||
说明:
|
||||
1. 本地构建 dist 并打包运行产物(不含 src/ 等开发源码)
|
||||
2. 上传到 103
|
||||
3. 103 全量备份当前 memind_adm
|
||||
4. 解包、安装依赖(不在 103 构建前端)
|
||||
5. 原子切换 live 目录并重启验证
|
||||
6. 旧 live 目录移入 archives,103 不再保留可编辑源码树
|
||||
|
||||
禁止:
|
||||
- 直接 rsync 到 103
|
||||
- 在 103 生产目录手改源码后继续运行
|
||||
- 在 103 保留 src/、vite.config.ts 等开发源码
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
--skip-build) SKIP_BUILD=1 ;;
|
||||
--yes|-y) AUTO_YES=1 ;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "未知参数: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
say() {
|
||||
printf '\n[%s] %s\n' "$(date +%H:%M:%S)" "$*"
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "缺少命令: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
need_cmd ssh
|
||||
need_cmd scp
|
||||
need_cmd tar
|
||||
need_cmd shasum
|
||||
|
||||
[[ -f "${IGNORE_FILE}" ]] || {
|
||||
echo "缺少忽略文件: ${IGNORE_FILE}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
say "本地预检查"
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" "echo release-ssh-ok" >/dev/null
|
||||
ssh -o BatchMode=yes "${HOST}" "test -d '${APP_DIR}' && test -f '${APP_DIR}/package.json'" >/dev/null
|
||||
|
||||
if [[ "${SKIP_BUILD}" -ne 1 ]]; then
|
||||
say "本地构建 dist(103 不再执行前端构建)"
|
||||
(
|
||||
cd "${ROOT}"
|
||||
VITE_BASE_PATH="${VITE_BASE_PATH:-/ops}" npm run build
|
||||
)
|
||||
fi
|
||||
|
||||
[[ -d "${ROOT}/dist" ]] || {
|
||||
echo "缺少 dist/,请先本地构建或去掉 --skip-build" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ -d "${MEMIND_SRC}" ]] || {
|
||||
echo "缺少 test-memind 共享模块源: ${MEMIND_SRC}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
say "打包 Memind 共享模块到 memind-lib"
|
||||
rm -rf "${MEMIND_LIB}"
|
||||
mkdir -p "${MEMIND_LIB}"
|
||||
find "${MEMIND_SRC}" -maxdepth 1 -name '*.mjs' -exec cp -f {} "${MEMIND_LIB}/" \;
|
||||
[[ -f "${MEMIND_LIB}/user-auth.mjs" ]] || {
|
||||
echo "memind-lib 缺少 user-auth.mjs" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ "${AUTO_YES}" -ne 1 && "${DRY_RUN}" -ne 1 ]]; then
|
||||
say "发布确认"
|
||||
echo "目标主机: ${HOST}"
|
||||
echo "目标目录: ${APP_DIR}"
|
||||
echo "发布编号: ${RELEASE_ID}"
|
||||
echo "本地 HEAD: $(git -C "${ROOT}" rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
echo "工作区状态:"
|
||||
git -C "${ROOT}" status --short --branch | sed -n '1,120p'
|
||||
read -r -p "确认继续发布到 103? [y/N] " confirm </dev/tty
|
||||
[[ "${confirm}" =~ ^[Yy]$ ]] || exit 0
|
||||
fi
|
||||
|
||||
say "生成发布清单"
|
||||
{
|
||||
echo "release_id=${RELEASE_ID}"
|
||||
echo "created_at=$(date '+%Y-%m-%d %H:%M:%S %z')"
|
||||
echo "host=$(hostname)"
|
||||
echo "root=${ROOT}"
|
||||
echo "git_head=$(git -C "${ROOT}" rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
echo "git_branch=$(git -C "${ROOT}" branch --show-current 2>/dev/null || echo detached)"
|
||||
echo "git_status_begin"
|
||||
git -C "${ROOT}" status --short --branch || true
|
||||
echo "git_status_end"
|
||||
} > "${MANIFEST_PATH}"
|
||||
|
||||
say "打包当前工作区"
|
||||
tar \
|
||||
--exclude-from="${IGNORE_FILE}" \
|
||||
-czf "${BUNDLE_PATH}" \
|
||||
-C "${ROOT}" \
|
||||
.
|
||||
checksum="$(shasum -a 256 "${BUNDLE_PATH}" | awk '{print $1}')"
|
||||
printf '%s %s\n' "${checksum}" "$(basename "${BUNDLE_PATH}")" > "${SHA_PATH}"
|
||||
|
||||
if [[ "${DRY_RUN}" -eq 1 ]]; then
|
||||
say "dry-run 完成"
|
||||
ls -lh "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
say "上传发布产物到 103"
|
||||
ssh -o BatchMode=yes "${HOST}" "mkdir -p '${INCOMING_DIR}' '${RELEASES_DIR}' '${BACKUP_DIR}' '${ARCHIVE_DIR}'"
|
||||
scp -q "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}" "${HOST}:${INCOMING_DIR}/"
|
||||
|
||||
say "在 103 备份并发布"
|
||||
ssh -o BatchMode=yes "${HOST}" \
|
||||
"RELEASE_ID='${RELEASE_ID}' APP_DIR='${APP_DIR}' INCOMING_DIR='${INCOMING_DIR}' RELEASES_DIR='${RELEASES_DIR}' BACKUP_DIR='${BACKUP_DIR}' ARCHIVE_DIR='${ARCHIVE_DIR}' WEB_HEALTH_URL='${WEB_HEALTH_URL}' API_HEALTH_URL='${API_HEALTH_URL}' /bin/bash" <<'REMOTE_SCRIPT'
|
||||
set -euo pipefail
|
||||
|
||||
RELEASE_DIR="${RELEASES_DIR}/memind-adm-${RELEASE_ID}"
|
||||
OLD_LIVE_DIR="${ARCHIVE_DIR}/memind_adm-source-before-${RELEASE_ID}"
|
||||
BUNDLE="${INCOMING_DIR}/memind-adm-${RELEASE_ID}.tar.gz"
|
||||
MANIFEST="${INCOMING_DIR}/memind-adm-${RELEASE_ID}.manifest.txt"
|
||||
SHA_FILE="${INCOMING_DIR}/memind-adm-${RELEASE_ID}.sha256"
|
||||
BACKUP_TAR="${BACKUP_DIR}/memind-adm-${RELEASE_ID}-before.tar.gz"
|
||||
PERSISTED_ITEMS=(
|
||||
".env"
|
||||
"adm-preview.log"
|
||||
"adm-api.log"
|
||||
)
|
||||
|
||||
say() {
|
||||
printf '\n[remote %s] %s\n' "$(date +%H:%M:%S)" "$*"
|
||||
}
|
||||
|
||||
rollback() {
|
||||
if [[ -d "${OLD_LIVE_DIR}" && ! -d "${APP_DIR}" ]]; then
|
||||
mv "${OLD_LIVE_DIR}" "${APP_DIR}"
|
||||
fi
|
||||
ADM_ROOT="${APP_DIR}" ADM_PORT=5174 ADM_API_PORT=8085 bash "${APP_DIR}/scripts/remote_restart.sh" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
trap 'rollback' ERR
|
||||
|
||||
[[ -f "${BUNDLE}" ]] || { echo "missing bundle: ${BUNDLE}" >&2; exit 1; }
|
||||
[[ -f "${MANIFEST}" ]] || { echo "missing manifest: ${MANIFEST}" >&2; exit 1; }
|
||||
[[ -f "${SHA_FILE}" ]] || { echo "missing sha file: ${SHA_FILE}" >&2; exit 1; }
|
||||
|
||||
say "校验产物完整性"
|
||||
cd "${INCOMING_DIR}"
|
||||
shasum -a 256 -c "$(basename "${SHA_FILE}")"
|
||||
|
||||
say "全量备份当前 live 目录"
|
||||
COPYFILE_DISABLE=1 tar -czf "${BACKUP_TAR}" -C "$(dirname "${APP_DIR}")" "$(basename "${APP_DIR}")"
|
||||
|
||||
say "准备 release 目录"
|
||||
rm -rf "${RELEASE_DIR}"
|
||||
mkdir -p "${RELEASE_DIR}"
|
||||
tar -xzf "${BUNDLE}" -C "${RELEASE_DIR}"
|
||||
cp "${MANIFEST}" "${RELEASE_DIR}/.release-manifest.txt"
|
||||
|
||||
for item in "${PERSISTED_ITEMS[@]}"; do
|
||||
if [[ -e "${APP_DIR}/${item}" ]]; then
|
||||
rm -rf "${RELEASE_DIR:?}/${item}"
|
||||
mkdir -p "$(dirname "${RELEASE_DIR}/${item}")"
|
||||
cp -a "${APP_DIR}/${item}" "$(dirname "${RELEASE_DIR}/${item}")/"
|
||||
fi
|
||||
done
|
||||
|
||||
[[ -d "${RELEASE_DIR}/dist" ]] || { echo "release bundle missing dist/" >&2; exit 1; }
|
||||
[[ ! -d "${RELEASE_DIR}/src" ]] || { echo "release bundle must not contain src/" >&2; exit 1; }
|
||||
|
||||
say "安装运行依赖"
|
||||
cd "${RELEASE_DIR}"
|
||||
export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/opt/homebrew/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
NPM_BIN="$(command -v npm)"
|
||||
elif [[ -x /opt/homebrew/opt/node@24/bin/npm ]]; then
|
||||
NPM_BIN="/opt/homebrew/opt/node@24/bin/npm"
|
||||
else
|
||||
echo "npm not found on remote" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -f package-lock.json ]]; then
|
||||
"${NPM_BIN}" ci --no-audit --no-fund
|
||||
else
|
||||
"${NPM_BIN}" install --no-audit --no-fund
|
||||
fi
|
||||
|
||||
cat > "${RELEASE_DIR}/PRODUCTION_ARTIFACT_ONLY.md" <<'ARTIFACT'
|
||||
This directory is a deployed production artifact on 103.
|
||||
Do not edit source files here.
|
||||
Source of truth: local repos under /Users/john/PycharmProjects/test/.
|
||||
Last artifact release manifest: .release-manifest.txt
|
||||
ARTIFACT
|
||||
|
||||
say "切换 live 目录"
|
||||
rm -rf "${OLD_LIVE_DIR}"
|
||||
mv "${APP_DIR}" "${OLD_LIVE_DIR}"
|
||||
mv "${RELEASE_DIR}" "${APP_DIR}"
|
||||
|
||||
say "重启服务"
|
||||
ADM_ROOT="${APP_DIR}" ADM_PORT=5174 ADM_API_PORT=8085 bash "${APP_DIR}/scripts/remote_restart.sh"
|
||||
|
||||
say "健康检查"
|
||||
api_code="$(curl -s -o /dev/null -w '%{http_code}' "${API_HEALTH_URL}" || true)"
|
||||
web_code="$(curl -s -o /dev/null -w '%{http_code}' "${WEB_HEALTH_URL}" || true)"
|
||||
if [[ "${api_code}" != "200" || "${web_code}" != "200" ]]; then
|
||||
echo "health check failed: api=${api_code} web=${web_code}" >&2
|
||||
rm -rf "${APP_DIR}"
|
||||
mv "${OLD_LIVE_DIR}" "${APP_DIR}"
|
||||
ADM_ROOT="${APP_DIR}" ADM_PORT=5174 ADM_API_PORT=8085 bash "${APP_DIR}/scripts/remote_restart.sh" >/dev/null 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
say "检查 live 目录不再保留开发源码"
|
||||
if [[ -d "${APP_DIR}/src" || -f "${APP_DIR}/vite.config.ts" ]]; then
|
||||
echo "source tree still present in live dir after switch" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
say "发布成功"
|
||||
rm -f "${BUNDLE}" "${MANIFEST}" "${SHA_FILE}"
|
||||
echo "backup_tar=${BACKUP_TAR}"
|
||||
echo "archived_source=${OLD_LIVE_DIR}"
|
||||
echo "live_dir=${APP_DIR}"
|
||||
REMOTE_SCRIPT
|
||||
|
||||
say "发布完成,读取线上版本摘要"
|
||||
ssh -o BatchMode=yes "${HOST}" "sed -n '1,20p' '${APP_DIR}/.release-manifest.txt' && echo '---' && test ! -d '${APP_DIR}/src' && echo 'src=absent' && curl -s '${API_HEALTH_URL}' && echo && curl -sI '${WEB_HEALTH_URL}' | head -1"
|
||||
@@ -16,6 +16,24 @@ command -v node >/dev/null 2>&1 || { echo "❌ 未找到 node" >&2; exit 1; }
|
||||
|
||||
cd "${ROOT}"
|
||||
|
||||
if [[ -f "${ROOT}/.env" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "${ROOT}/.env"
|
||||
set +a
|
||||
fi
|
||||
|
||||
BASE_PATH="${VITE_BASE_PATH:-/ops}"
|
||||
if [[ "${BASE_PATH}" != */ ]]; then
|
||||
BASE_PATH="${BASE_PATH}/"
|
||||
fi
|
||||
API_HOST="${ADM_API_HOST:-127.0.0.1}"
|
||||
WEB_HOST="${ADM_WEB_HOST:-${API_HOST}}"
|
||||
export ADM_API_HOST="${API_HOST}"
|
||||
export ADM_WEB_HOST="${WEB_HOST}"
|
||||
export ADM_PORT="${PORT}"
|
||||
export VITE_BASE_PATH="${BASE_PATH%/}"
|
||||
|
||||
if [[ ! -d dist ]]; then
|
||||
echo "❌ ${ROOT}/dist 不存在,请先构建或 rsync dist" >&2
|
||||
exit 1
|
||||
@@ -27,13 +45,20 @@ if [[ ! -d node_modules ]]; then
|
||||
fi
|
||||
|
||||
echo "==> 停止旧进程 (port ${PORT}, ${API_PORT})..."
|
||||
if [[ -f "${PID_FILE}" ]]; then
|
||||
kill "$(cat "${PID_FILE}")" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -f "${API_PID_FILE}" ]]; then
|
||||
kill "$(cat "${API_PID_FILE}")" 2>/dev/null || true
|
||||
fi
|
||||
lsof -ti "TCP:${PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
|
||||
lsof -ti "TCP:${API_PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
|
||||
sleep 1
|
||||
sleep 2
|
||||
lsof -ti "TCP:${PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null || true
|
||||
lsof -ti "TCP:${API_PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
echo "==> 启动 Admin API @ 127.0.0.1:${API_PORT}..."
|
||||
echo "==> 启动 Admin API @ ${API_HOST}:${API_PORT}..."
|
||||
nohup node server/index.mjs >>"${API_LOG}" 2>&1 &
|
||||
echo $! >"${API_PID_FILE}"
|
||||
sleep 2
|
||||
@@ -44,14 +69,17 @@ if ! curl -sf "http://127.0.0.1:${API_PORT}/health" >/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> 启动 vite preview @ 127.0.0.1:${PORT}..."
|
||||
nohup npm run dev:preview -- --host 127.0.0.1 --port "${PORT}" >>"${LOG}" 2>&1 &
|
||||
echo "==> 启动 vite preview @ ${WEB_HOST}:${PORT} (base=${BASE_PATH})..."
|
||||
nohup npx vite preview --config scripts/vite-preview.config.mjs >>"${LOG}" 2>&1 &
|
||||
echo $! >"${PID_FILE}"
|
||||
sleep 2
|
||||
|
||||
if curl -sf "http://127.0.0.1:${PORT}/" >/dev/null; then
|
||||
echo "✅ memind_adm 已就绪: http://127.0.0.1:${PORT}/"
|
||||
curl -sf "http://127.0.0.1:${PORT}/" >/dev/null && echo " 首页 OK"
|
||||
preview_url="http://127.0.0.1:${PORT}${BASE_PATH}"
|
||||
login_probe="http://127.0.0.1:${PORT}/auth/status"
|
||||
if curl -sf "${preview_url}" >/dev/null && curl -sf "${login_probe}" >/dev/null; then
|
||||
echo "✅ memind_adm 已就绪: ${preview_url}"
|
||||
curl -sf "${preview_url}" >/dev/null && echo " 首页 OK"
|
||||
echo " /auth 反代 OK"
|
||||
echo " 登录请使用数据库中已初始化的 admin 账号"
|
||||
else
|
||||
echo "❌ 启动失败,最近日志:" >&2
|
||||
|
||||
+10
-84
@@ -1,90 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# memind_adm → 100 服务器 rsync 部署
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/rsync_to_server.sh
|
||||
# ./scripts/rsync_to_server.sh --skip-build
|
||||
# ./scripts/rsync_to_server.sh --no-restart
|
||||
# SKIP_BUILD=1 NO_RESTART=1 ./scripts/rsync_to_server.sh
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
EXCLUDE="${ROOT}/scripts/.rsync-exclude-lan"
|
||||
DEPLOY_HOST="${DEPLOY_HOST:-john@100.99.38.66}"
|
||||
REMOTE_DIR="${REMOTE_DIR:-/Users/john/Project/memind_adm}"
|
||||
ADM_PORT="${ADM_PORT:-5174}"
|
||||
SSH_OPTS=(-o ConnectTimeout=15 -o BatchMode=yes -o StrictHostKeyChecking=accept-new)
|
||||
cat >&2 <<'EOF'
|
||||
`scripts/rsync_to_server.sh` 已禁用。
|
||||
|
||||
SKIP_BUILD=0
|
||||
NO_RESTART=0
|
||||
103 / Studio 生产发布禁止再走 rsync。
|
||||
请改用:
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--skip-build) SKIP_BUILD=1 ;;
|
||||
--no-restart) NO_RESTART=1 ;;
|
||||
-h|--help)
|
||||
sed -n '2,8p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "未知参数: $arg" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
bash scripts/release-prod.sh
|
||||
|
||||
ssh_cmd() {
|
||||
ssh "${SSH_OPTS[@]}" "${DEPLOY_HOST}" "$@"
|
||||
}
|
||||
|
||||
require_ssh() {
|
||||
if ! ssh_cmd 'echo ok' >/dev/null 2>&1; then
|
||||
echo "❌ 无法 SSH 到 ${DEPLOY_HOST}" >&2
|
||||
echo "请确认 Tailscale 已连接,或执行: ssh-copy-id ${DEPLOY_HOST}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "======================================"
|
||||
echo "memind_adm → ${DEPLOY_HOST}"
|
||||
echo "时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "本地: ${ROOT}"
|
||||
echo "远端: ${REMOTE_DIR}"
|
||||
echo "端口: ${ADM_PORT}"
|
||||
echo "======================================"
|
||||
|
||||
require_ssh
|
||||
|
||||
if [[ "${SKIP_BUILD}" -eq 0 ]]; then
|
||||
echo "==> 本地构建..."
|
||||
cd "${ROOT}"
|
||||
npm run build
|
||||
fi
|
||||
|
||||
echo "==> 创建远端目录..."
|
||||
ssh_cmd "mkdir -p '${REMOTE_DIR}'"
|
||||
|
||||
echo "==> rsync 源码 (--delete)..."
|
||||
rsync -az --delete --exclude-from="${EXCLUDE}" \
|
||||
"${ROOT}/" \
|
||||
"${DEPLOY_HOST}:${REMOTE_DIR}/"
|
||||
|
||||
echo "==> rsync dist..."
|
||||
rsync -az --delete \
|
||||
"${ROOT}/dist/" \
|
||||
"${DEPLOY_HOST}:${REMOTE_DIR}/dist/"
|
||||
|
||||
echo "==> 写入远端 .env(不覆盖已有)..."
|
||||
ssh_cmd "test -f '${REMOTE_DIR}/.env' || cat > '${REMOTE_DIR}/.env' <<'EOF'
|
||||
ADM_DEV_BACKEND=http://127.0.0.1:8081
|
||||
VITE_MAIN_APP_URL=http://127.0.0.1:8081
|
||||
EOF"
|
||||
|
||||
if [[ "${NO_RESTART}" -eq 0 ]]; then
|
||||
echo "==> 远端重启服务..."
|
||||
ssh_cmd "ADM_ROOT='${REMOTE_DIR}' ADM_PORT='${ADM_PORT}' bash -s" <"${ROOT}/scripts/remote_restart.sh"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ 已部署到 ${DEPLOY_HOST}:${REMOTE_DIR}"
|
||||
echo " 本机访问: ssh ${DEPLOY_HOST} 'curl -sI http://127.0.0.1:${ADM_PORT}/ | head -1'"
|
||||
规则见:
|
||||
- PRODUCTION_RELEASE_RULES.md
|
||||
- docs/DEPLOY.md
|
||||
EOF
|
||||
exit 1
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '');
|
||||
const backend = env.ADM_DEV_BACKEND ?? 'http://127.0.0.1:8085';
|
||||
const basePath = env.VITE_BASE_PATH?.trim() || '/ops';
|
||||
const base = basePath.endsWith('/') ? basePath : `${basePath}/`;
|
||||
|
||||
return {
|
||||
base,
|
||||
preview: {
|
||||
host: env.ADM_WEB_HOST ?? env.ADM_API_HOST ?? '127.0.0.1',
|
||||
port: Number(env.ADM_PORT ?? 5174),
|
||||
strictPort: true,
|
||||
allowedHosts: ['gadm.tkmind.cn', 'localhost', '127.0.0.1', '10.10.0.2', '58.38.22.103'],
|
||||
proxy: {
|
||||
'/api': backend,
|
||||
'/auth': backend,
|
||||
'/admin-api': backend,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user