diff --git a/scripts/build-portal-runtime.mjs b/scripts/build-portal-runtime.mjs index 15ad61d..fc6736a 100755 --- a/scripts/build-portal-runtime.mjs +++ b/scripts/build-portal-runtime.mjs @@ -181,8 +181,18 @@ async function writeMetadata() { type: 'module', }; await writeFile(path.join(runtimeRoot, 'package.json'), `${JSON.stringify(runtimePackageJson, null, 2)}\n`); + const tunnelScript = path.join(root, 'scripts', 'memind-portal-tunnel.sh'); + const runtimeScriptsDir = path.join(runtimeRoot, 'scripts'); + await fs.mkdir(runtimeScriptsDir, { recursive: true }); + if (await exists(tunnelScript)) { + await fs.copyFile(tunnelScript, path.join(runtimeScriptsDir, 'memind-portal-tunnel.sh')); + await fs.chmod(path.join(runtimeScriptsDir, 'memind-portal-tunnel.sh'), 0o755); + } else { + throw new Error(`缺少隧道脚本: ${tunnelScript}`); + } + await writeFile( - path.join(runtimeRoot, 'scripts', 'run-memind-portal-prod.sh'), + path.join(runtimeScriptsDir, 'run-memind-portal-prod.sh'), [ '#!/usr/bin/env bash', 'set -euo pipefail', @@ -242,6 +252,8 @@ async function writeMetadata() { 'Deployment and operations transport:', ' 105 fixed IP: 120.26.184.105', ' 103 / Studio fixed IP: 58.38.22.103', + ' m.tkmind.cn: 105 nginx -> 127.0.0.1:19081 -> reverse SSH tunnel -> Portal :8081', + ' scripts/memind-portal-tunnel.sh must stay in runtime; release restarts cn.tkmind.memind-portal-tunnel', ' Do not switch back to 10.10.* LAN paths unless explicitly required.', '', ].join('\n'), @@ -256,6 +268,7 @@ async function main() { await copyNodeModules(); await writeMetadata(); await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-portal-prod.sh'), 0o755); + await fs.chmod(path.join(runtimeRoot, 'scripts', 'memind-portal-tunnel.sh'), 0o755); console.log(''); console.log(`Portal runtime 已生成: ${runtimeRoot}`); } diff --git a/scripts/release-portal-runtime-prod.sh b/scripts/release-portal-runtime-prod.sh index 647ee4e..7648962 100755 --- a/scripts/release-portal-runtime-prod.sh +++ b/scripts/release-portal-runtime-prod.sh @@ -10,6 +10,9 @@ BACKUP_DIR="${REMOTE_ROOT}/backups/memind" ARCHIVE_DIR="${REMOTE_ROOT}/archives" HEALTH_URL="${STUDIO_HEALTH_URL:-http://127.0.0.1:8081/api/status}" PORTAL_LABEL="cn.tkmind.memind-portal" +PORTAL_TUNNEL_LABEL="cn.tkmind.memind-portal-tunnel" +MEMIND_PORTAL_TUNNEL_HOST="${MEMIND_PORTAL_TUNNEL_HOST:-ssh105-public}" +MEMIND_PORTAL_TUNNEL_REMOTE_PORT="${MEMIND_PORTAL_TUNNEL_REMOTE_PORT:-19081}" LAUNCHD_GUI="gui/$(id -u)" RELEASE_TS="$(date +%Y%m%d-%H%M%S)" SHORT_SHA="$(git -C "${ROOT}" rev-parse --short HEAD 2>/dev/null || echo no-git)" @@ -41,7 +44,8 @@ usage() { 4. 停止旧 Portal 服务 5. 用 runtime artifact 替换 live 目录,只继承持久目录 6. 启动 Portal,保持旧端口 8081 - 7. 健康检查通过后,旧源码目录移入 archive,不再保留可运行 live 源码 + 7. 健康检查通过后重启 m.tkmind.cn 反向隧道 (105:19081 -> 103:8081) + 8. 旧源码目录移入 archive,不再保留可运行 live 源码 EOF } @@ -139,7 +143,7 @@ fi verify_runtime_artifact() { local missing=0 - for required in server.mjs mindspace-sandbox-mcp.mjs dist package.json scripts/run-memind-portal-prod.sh; do + for required in server.mjs mindspace-sandbox-mcp.mjs dist package.json scripts/run-memind-portal-prod.sh scripts/memind-portal-tunnel.sh; do if [[ ! -e "${RUNTIME_ROOT}/${required}" ]]; then echo "runtime 产物缺失: ${RUNTIME_ROOT}/${required}" >&2 missing=1 @@ -273,7 +277,7 @@ scp -q "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}" "${HOST}:${INCOMING_DIR say "在 103 停旧服务并切换到无源码 runtime" ssh -o BatchMode=yes "${HOST}" \ - "RELEASE_ID='${RELEASE_ID}' APP_DIR='${APP_DIR}' INCOMING_DIR='${INCOMING_DIR}' BACKUP_DIR='${BACKUP_DIR}' ARCHIVE_DIR='${ARCHIVE_DIR}' HEALTH_URL='${HEALTH_URL}' PORTAL_LABEL='${PORTAL_LABEL}' LAUNCHD_GUI='${LAUNCHD_GUI}' /bin/bash" <<'REMOTE_SCRIPT' + "RELEASE_ID='${RELEASE_ID}' APP_DIR='${APP_DIR}' INCOMING_DIR='${INCOMING_DIR}' BACKUP_DIR='${BACKUP_DIR}' ARCHIVE_DIR='${ARCHIVE_DIR}' HEALTH_URL='${HEALTH_URL}' PORTAL_LABEL='${PORTAL_LABEL}' PORTAL_TUNNEL_LABEL='${PORTAL_TUNNEL_LABEL}' MEMIND_PORTAL_TUNNEL_HOST='${MEMIND_PORTAL_TUNNEL_HOST}' MEMIND_PORTAL_TUNNEL_REMOTE_PORT='${MEMIND_PORTAL_TUNNEL_REMOTE_PORT}' LAUNCHD_GUI='${LAUNCHD_GUI}' /bin/bash" <<'REMOTE_SCRIPT' set -euo pipefail RUNTIME_DIR="${APP_DIR}.runtime-${RELEASE_ID}" @@ -297,6 +301,67 @@ say() { printf '\n[remote %s] %s\n' "$(date +%H:%M:%S)" "$*" } +ensure_portal_tunnel() { + local tunnel_script="${APP_DIR}/scripts/memind-portal-tunnel.sh" + local tunnel_label="${PORTAL_TUNNEL_LABEL:-cn.tkmind.memind-portal-tunnel}" + local tunnel_host="${MEMIND_PORTAL_TUNNEL_HOST:-ssh105-public}" + local remote_port="${MEMIND_PORTAL_TUNNEL_REMOTE_PORT:-19081}" + + [[ -x "${tunnel_script}" ]] || { + echo "missing tunnel script: ${tunnel_script}" >&2 + return 1 + } + + cat > "${HOME}/Library/LaunchAgents/${tunnel_label}.plist" < + + + + Label + ${tunnel_label} + ProgramArguments + + ${tunnel_script} + + EnvironmentVariables + + MEMIND_PORTAL_TUNNEL_HOST + ${tunnel_host} + + RunAtLoad + + KeepAlive + + StandardOutPath + ${HOME}/Library/Logs/memind-portal-tunnel.log + StandardErrorPath + ${HOME}/Library/Logs/memind-portal-tunnel.log + + +EOF + + launchctl bootout "${LAUNCHD_GUI}/${tunnel_label}" >/dev/null 2>&1 || true + launchctl bootstrap "${LAUNCHD_GUI}" "${HOME}/Library/LaunchAgents/${tunnel_label}.plist" >/dev/null 2>&1 || true + launchctl enable "${LAUNCHD_GUI}/${tunnel_label}" >/dev/null 2>&1 || true + launchctl kickstart -k "${LAUNCHD_GUI}/${tunnel_label}" >/dev/null 2>&1 || true + sleep 2 + + if ! launchctl print "${LAUNCHD_GUI}/${tunnel_label}" 2>/dev/null | grep -q 'state = running'; then + echo "portal tunnel failed to start (${tunnel_label})" >&2 + return 1 + fi + + if command -v ssh >/dev/null 2>&1; then + tunnel_code="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "${tunnel_host}" \ + "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${remote_port}/api/status" 2>/dev/null || true)" + if [[ "${tunnel_code}" == "200" ]]; then + say "105 隧道端口 ${remote_port} 健康" + else + echo "warn: 105:${remote_port} 健康检查未通过 (code=${tunnel_code:-000});请确认 105 nginx upstream 指向 127.0.0.1:${remote_port}" >&2 + fi + fi +} + rollback() { if [[ -d "${OLD_LIVE_DIR}" && ! -d "${APP_DIR}" ]]; then mv "${OLD_LIVE_DIR}" "${APP_DIR}" @@ -306,6 +371,7 @@ rollback() { if ! curl -sf "${HEALTH_URL}" >/dev/null 2>&1; then nohup "${APP_DIR}/scripts/run-memind-portal-prod.sh" >> "${HOME}/Library/Logs/memind-portal.log" 2>&1 & fi + ensure_portal_tunnel >/dev/null 2>&1 || true } trap 'rollback' ERR @@ -435,6 +501,9 @@ if [[ "${portal_code}" != "200" ]]; then exit 1 fi +say "重启 m.tkmind.cn 反向隧道" +ensure_portal_tunnel + say "检查 live 目录中不再保留源码树" allowed_live_mjs=( "${APP_DIR}/server.mjs"