diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 1f77557c..11bd0e53 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -109,6 +109,7 @@ - 背景:`Genarrative-Server-Provision` 的 `DEPLOY_TARGET=development` 语义是部署到 dev 服务器,不是构建机 dry-run。旧流水线把 development 映射到 `linux && genarrative-build`,还先在 build 节点准备 `provision-tools/` 再 stash 给后续阶段,导致真实 dev 初始化可能跑到 Jenkins controller / build 节点;脚本还安装 clang / lld / pkg-config / OpenSSL headers / sccache 等构建链依赖,超出了服务器初始化职责。 - 决策:Server-Provision 只做服务器初始化,全程运行在目标部署 agent:development 使用 `linux && genarrative-dev-deploy`,release 使用 `linux && genarrative-release-deploy`。`Prepare Provision Tools` 与 `Provision Server` 在同一个目标 agent workspace 顺序执行,不再切到 `linux && genarrative-build`,不再 `stash/unstash` 工具包。`scripts/jenkins-server-provision.sh` 不再安装 clang / lld / pkg-config / libssl-dev / sccache;非 dry-run 仍要求目标 dev / release agent 具备 root 权限,因为 provision 会写 systemd、Nginx、`/etc` 和系统用户。Job 的 `Pipeline script from SCM` 与 Jenkinsfile 参数 `SOURCE_GIT_REMOTE_URL` 都必须使用本机路径或目标 agent 可访问的内网 Git 源,不允许公网 Git fallback。 +- 追加决策(2026-06-10):`Prepare Provision Tools` 必须先读取目标机现状,再准备需要的文件。目标机 `/usr/local/bin/otelcol-contrib` 版本匹配 `OTELCOL_VERSION` 时直接复用;`${SPACETIME_ROOT}/bin/current/spacetimedb-cli` 和 `spacetimedb-standalone` 存在且 CLI 版本匹配 `SPACETIME_EXPECTED_VERSION` 或 `SPACETIME_DOWNLOAD_ROOT` 中的版本时,直接复用当前安装生成 `provision-tools/`。只有目标机缺失、不可执行或版本不匹配时,才消费 `PROVISION_DOWNLOADS_DIR` 中的本地包或进入下载分支。 - 影响范围:`jenkins/Jenkinsfile.production-server-provision`、`scripts/jenkins-server-provision.sh`、生产运维文档、Server-Provision 排障口径。 - 验证方式:Jenkins 日志中 Server-Provision 的 `Prepare`、`Checkout Provision Files`、`Prepare Provision Tools` 和 `Provision Server` 都在目标 dev / release agent 上执行;日志不出现 `Running on Jenkins`、`linux && genarrative-build`、`stash 'server-provision-tools'`、`Git 主地址拉取失败...改用备用地址`、`https://git.genarrative.world/GenarrativeAI/Genarrative.git` 或构建依赖 / sccache 安装步骤;`bash -n scripts/jenkins-server-provision.sh` 和编码检查通过。 - 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 2972bbf6..1b904428 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -1314,6 +1314,14 @@ - 验证:Jenkins 日志中 `Provision Target` 下的 `Prepare`、`Checkout Provision Files`、`Prepare Provision Tools` 和 `Provision Server` 都应运行在目标 dev / release agent;日志不应出现 `stash 'server-provision-tools'`、目标阶段 `unstash`、`Git 主地址拉取失败...改用备用地址` 或 `https://git.genarrative.world/GenarrativeAI/Genarrative.git`。 - 关联:`jenkins/Jenkinsfile.production-server-provision`、`scripts/prepare-server-provision-tools.sh`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 +## Server-Provision 不要无条件下载工具包 + +- 现象:目标 dev / release 机器已经安装正确版本的 SpacetimeDB 或 `otelcol-contrib`,但 `Prepare Provision Tools` 仍每次下载 release tarball,网络慢或 GitHub 不稳时会把服务器初始化卡在准备阶段。 +- 原因:工具准备阶段如果只按“生成交付包”理解,会忽略它已经运行在目标部署 agent 上这一事实;此时目标机本地的 `/usr/local/bin/otelcol-contrib` 与 `${SPACETIME_ROOT}/bin/current` 就是可信状态源。 +- 处理:`scripts/prepare-server-provision-tools.sh` 必须先检查目标机状态:`otelcol-contrib --version` 命中 `OTELCOL_VERSION` 时复制现有二进制;`spacetimedb-cli --version` 命中 `SPACETIME_EXPECTED_VERSION` 或 `SPACETIME_DOWNLOAD_ROOT` 推导出的版本且 standalone 同时存在时,复制 `${SPACETIME_ROOT}/bin` 并生成 wrapper。只有缺失、不可执行或版本不匹配时,才查 `PROVISION_DOWNLOADS_DIR` 或下载源。 +- 验证:运行 `bash scripts/check-server-provision-tools.sh`;Jenkins 日志应先出现“检查目标机 ...”,已有版本命中时出现“复用目标机已有 ...”,且不出现“下载 ...”。 +- 关联:`scripts/prepare-server-provision-tools.sh`、`jenkins/Jenkinsfile.production-server-provision`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + ## 个人任务 scope 不得扩成 work/site/module - 现象:个人任务配置为 `work` / `site` / `module` 后进度串桶或静默按 0 处理。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 400ae33a..4797b4d4 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -277,7 +277,7 @@ dev 服务器上的 Gitea 内网入口固定为 `http://10.2.0.10/GenarrativeAI/ - `GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=512` 开启应用内 HTTP 并发背压;`GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320`、`GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS=64`、`GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS=16` 分别限制公开列表、公开详情和后台 API 热路径。超过许可时直接返回 `429 Too Many Requests` 和 `Retry-After: 1`,`/healthz` 与 `/readyz` 不受该限制。这些值不是 RPS 限速;如果压测中 429 上升但内存和 p95 收敛,说明背压正在保护进程。直连 `api-server` 的极高 RPS 压测若出现 `connection refused`,通常已经打到 TCP 监听 / accept 层,应同时检查 backlog、Nginx upstream keepalive 和前置限流。 - `api-server` 正常运行时 `/healthz` 返回进程存活状态,`/readyz` 返回是否仍接收新流量;收到 `SIGINT` / `SIGTERM` 后会先把 readiness 标记为不可用,再让 Axum 停止接新连接并等待已有 HTTP 请求排空。systemd 仍以 `KillSignal=SIGINT` 停服务,`TimeoutStopSec=90` 作为长请求排空上限。 - `genarrative-api.service` 设置 `LimitNOFILE=65535`、`TasksMax=2048`;上线后用 `systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax -p TimeoutStopUSec` 和 `cat /proc/$(pidof api-server)/limits` 核对。 -- Server provision 不再通过 Windows helper 下载,也不再通过 Linux build 节点中转工具包。`Prepare Provision Tools` 在目标 dev / release agent 工作区内准备 SpacetimeDB `2.4.1` 的 `spacetime-x86_64-unknown-linux-gnu.tar.gz` 和 `otelcol-contrib_0.151.0_linux_amd64.tar.gz` 并生成 `provision-tools/`;如果目标服务器下载需要代理,在 `PROVISION_DOWNLOAD_PROXY` 配置目标机可访问的 HTTP 代理。 +- Server provision 不再通过 Windows helper 下载,也不再通过 Linux build 节点中转工具包。`Prepare Provision Tools` 在目标 dev / release agent 工作区内先检查 `/usr/local/bin/otelcol-contrib` 与 `${SPACETIME_ROOT}/bin/current`:版本已满足时直接复用目标机现有文件生成 `provision-tools/`,只有缺失或版本不匹配时才使用 `PROVISION_DOWNLOADS_DIR` 里的本地包或从配置的下载源准备 SpacetimeDB `2.4.1` / `otelcol-contrib 0.151.0`;如果目标服务器下载需要代理,在 `PROVISION_DOWNLOAD_PROXY` 配置目标机可访问的 HTTP 代理。 - 除 `Genarrative-Server-Provision` 外,`Genarrative-Stdb-Module-Build`、`Genarrative-Web-Build`、`Genarrative-Api-Build`、`Genarrative-*Deploy`、`Genarrative-Database-Import/Export`、`Genarrative-Full-Build-And-Deploy` 和 `Genarrative-Notify-Email` 的生产流水线现都以 Linux agent 为主,仍按各自 Jenkinsfile 的 checkout 口径执行。Server provision 不使用公网备用 Git 源。 - `otelcol-contrib.service` 作为可选系统服务加入 provision,默认监听 `127.0.0.1:4317/4318` 并使用 `deploy/otelcol/genarrative-debug.yaml`。api-server 是否发送 OTLP 仍由 `GENARRATIVE_OTEL_ENABLED` 控制,服务 unit 见 `deploy/systemd/otelcol-contrib.service`。 - Nginx `/api/` 与 `/admin/api/` 通过 `genarrative_api` upstream 代理到 `127.0.0.1:8082`,upstream keepalive 为 64;`limit_conn` 负责连接 / 并发保护,`limit_req` 负责入口 RPS 快拒绝。当前模板把公开 gallery list 单独放到 `genarrative_gallery_rps`,默认 `rate=5000r/s`、`burst=4096`、`limit_conn=320`;公开详情和普通 API 放到 `genarrative_api_rps`,后台 API 放到 `genarrative_admin_rps`。通用 `/api` location 设置 `client_max_body_size 64m` 是反代兜底,防止拼图入口页 / 新增关卡本地参考图 Data URL 或旧兼容请求在到达 `api-server` 前被默认 1 MiB 上限拦截;拼图本地参考图前后端统一限制 6MB,历史图片仍提交 `referenceImageAssetObjectId(s)`。若线上出现 `413 Request Entity Too Large` 且 access log 中 `request_time=0.000`、`upstream_status=-`,说明请求在 Nginx 层被拦截,先用 `nginx -T | grep client_max_body_size` 检查 release 模板是否已渲染并 reload,同时检查前端是否超出 6MB 或错误提交了未压缩大图。`limit_conn_status 429` 和 `limit_req_status 429` 必须在 HTTP 与 HTTPS server 中同时生效;若线上压测看到 `limiting connections by zone "genarrative_api_conn"` 却返回 503,优先检查 `nginx -T` 里 HTTPS server 是否缺少这些状态码,以及 `/api/runtime/puzzle/gallery` 是否误落到通用 `location ~ ^/api` 的 `limit_conn=64`。压测时看 `/var/log/nginx/genarrative.access.log` 中的 `request_time`、`upstream_connect_time`、`upstream_header_time`、`upstream_response_time`、`upstream_status`、`request_id`。 diff --git a/jenkins/Jenkinsfile.production-server-provision b/jenkins/Jenkinsfile.production-server-provision index 9e6da12d..eb5ceb77 100644 --- a/jenkins/Jenkinsfile.production-server-provision +++ b/jenkins/Jenkinsfile.production-server-provision @@ -164,6 +164,7 @@ BASH PROVISION_DOWNLOAD_PROXY="${PROVISION_DOWNLOAD_PROXY:-}" \ SPACETIME_DOWNLOAD_ROOT="${SPACETIME_DOWNLOAD_ROOT:-https://github.com/clockworklabs/SpacetimeDB/releases/download/v2.4.1}" \ SPACETIME_TARGET_HOST="${SPACETIME_TARGET_HOST:-x86_64-unknown-linux-gnu}" \ + SPACETIME_ROOT="${SPACETIME_ROOT:-/stdb}" \ scripts/prepare-server-provision-tools.sh ' ''' diff --git a/scripts/check-server-provision-tools.sh b/scripts/check-server-provision-tools.sh new file mode 100755 index 00000000..0401529b --- /dev/null +++ b/scripts/check-server-provision-tools.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +TMP_ROOT="$(mktemp -d)" +trap 'rm -rf "${TMP_ROOT}"' EXIT + +WORK_DIR="${TMP_ROOT}/workspace" +FAKE_BIN_DIR="${TMP_ROOT}/fake-bin" +TARGET_BIN_DIR="${TMP_ROOT}/target-bin" +SPACETIME_ROOT_DIR="${TMP_ROOT}/stdb" +OUTPUT_LOG="${TMP_ROOT}/prepare.log" + +mkdir -p \ + "${WORK_DIR}" \ + "${FAKE_BIN_DIR}" \ + "${TARGET_BIN_DIR}" \ + "${SPACETIME_ROOT_DIR}/bin/current" + +cat >"${FAKE_BIN_DIR}/curl" <<'EOF' +#!/usr/bin/env bash +echo "curl should not be called when target tools are already ready" >&2 +exit 97 +EOF +cat >"${FAKE_BIN_DIR}/wget" <<'EOF' +#!/usr/bin/env bash +echo "wget should not be called when target tools are already ready" >&2 +exit 97 +EOF +chmod +x "${FAKE_BIN_DIR}/curl" "${FAKE_BIN_DIR}/wget" + +cat >"${TARGET_BIN_DIR}/otelcol-contrib" <<'EOF' +#!/usr/bin/env bash +echo "otelcol-contrib version 0.151.0" +EOF +chmod +x "${TARGET_BIN_DIR}/otelcol-contrib" + +cat >"${SPACETIME_ROOT_DIR}/bin/current/spacetimedb-cli" <<'EOF' +#!/usr/bin/env bash +echo "spacetimedb-cli 2.4.1" +EOF +cat >"${SPACETIME_ROOT_DIR}/bin/current/spacetimedb-standalone" <<'EOF' +#!/usr/bin/env bash +echo "spacetimedb-standalone 2.4.1" +EOF +chmod +x \ + "${SPACETIME_ROOT_DIR}/bin/current/spacetimedb-cli" \ + "${SPACETIME_ROOT_DIR}/bin/current/spacetimedb-standalone" + +if ! ( + cd "${WORK_DIR}" + PATH="${FAKE_BIN_DIR}:${PATH}" \ + WORKSPACE="${WORK_DIR}" \ + PROVISION_TOOLS_DIR="provision-tools" \ + PROVISION_DOWNLOADS_DIR="downloads" \ + PROVISION_TOOLS_TMP_PARENT="${WORK_DIR}/.tmp/server-provision-tools" \ + PROVISION_REQUIRE_LOCAL_DOWNLOADS="true" \ + OTELCOL_TARGET_BIN="${TARGET_BIN_DIR}/otelcol-contrib" \ + OTELCOL_VERSION="0.151.0" \ + SPACETIME_ROOT="${SPACETIME_ROOT_DIR}" \ + SPACETIME_EXPECTED_VERSION="2.4.1" \ + "${REPO_ROOT}/scripts/prepare-server-provision-tools.sh" \ + >"${OUTPUT_LOG}" 2>&1 +); then + echo "[check-server-provision-tools] prepare-server-provision-tools.sh 执行失败。" >&2 + cat "${OUTPUT_LOG}" >&2 + exit 1 +fi + +grep -q "复用目标机已有 otelcol-contrib" "${OUTPUT_LOG}" +grep -q "复用目标机已有 SpacetimeDB 安装" "${OUTPUT_LOG}" +grep -q "otelcol-contrib 0.151.0 target existing" "${WORK_DIR}/provision-tools/MANIFEST.txt" +grep -q "spacetime target existing" "${WORK_DIR}/provision-tools/MANIFEST.txt" + +test -x "${WORK_DIR}/provision-tools/otelcol-contrib" +test -x "${WORK_DIR}/provision-tools/spacetime/spacetime" +test -x "${WORK_DIR}/provision-tools/spacetime/bin/current/spacetimedb-cli" +test -x "${WORK_DIR}/provision-tools/spacetime/bin/current/spacetimedb-standalone" + +if grep -q "下载 " "${OUTPUT_LOG}"; then + echo "[check-server-provision-tools] 已有目标机工具时不应进入下载分支。" >&2 + cat "${OUTPUT_LOG}" >&2 + exit 1 +fi + +echo "[check-server-provision-tools] OK" diff --git a/scripts/prepare-server-provision-tools.sh b/scripts/prepare-server-provision-tools.sh index 8bbd8266..edb44a12 100755 --- a/scripts/prepare-server-provision-tools.sh +++ b/scripts/prepare-server-provision-tools.sh @@ -7,9 +7,12 @@ OTELCOL_VERSION="${OTELCOL_VERSION:-0.151.0}" PREPARE_OTELCOL="${PREPARE_OTELCOL:-${ENABLE_OTELCOL:-true}}" OTELCOL_DOWNLOAD_ROOT="${OTELCOL_DOWNLOAD_ROOT:-https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download}" OTELCOL_ARCHIVE_PATH="${OTELCOL_ARCHIVE_PATH:-}" +OTELCOL_TARGET_BIN="${OTELCOL_TARGET_BIN:-/usr/local/bin/otelcol-contrib}" SPACETIME_INSTALLER_URL="${SPACETIME_INSTALLER_URL:-https://install.spacetimedb.com}" SPACETIME_DOWNLOAD_ROOT="${SPACETIME_DOWNLOAD_ROOT:-https://github.com/clockworklabs/SpacetimeDB/releases/download/v2.4.1}" SPACETIME_TARGET_HOST="${SPACETIME_TARGET_HOST:-x86_64-unknown-linux-gnu}" +SPACETIME_ROOT="${SPACETIME_ROOT:-/stdb}" +SPACETIME_EXPECTED_VERSION="${SPACETIME_EXPECTED_VERSION:-}" SPACETIME_ARCHIVE_PATH="${SPACETIME_ARCHIVE_PATH:-}" SPACETIME_INSTALLER_PATH="${SPACETIME_INSTALLER_PATH:-}" SPACETIME_UPDATE_INSTALLER_PATH="${SPACETIME_UPDATE_INSTALLER_PATH:-}" @@ -65,6 +68,60 @@ download_file() { fi } +resolve_spacetime_expected_version() { + local download_root="${SPACETIME_DOWNLOAD_ROOT%/}" + + if [[ -n "${SPACETIME_EXPECTED_VERSION}" ]]; then + printf "%s" "${SPACETIME_EXPECTED_VERSION}" + return + fi + + if [[ "${download_root}" =~ /v([0-9]+(\.[0-9]+){1,2})$ ]]; then + printf "%s" "${BASH_REMATCH[1]}" + fi +} + +target_otelcol_ready() { + local version_output + + echo "[prepare-provision-tools] 检查目标机 otelcol-contrib: ${OTELCOL_TARGET_BIN}" + if [[ ! -x "${OTELCOL_TARGET_BIN}" ]]; then + echo "[prepare-provision-tools] 目标机 otelcol-contrib 不存在或不可执行,将准备交付文件。" + return 1 + fi + + version_output="$("${OTELCOL_TARGET_BIN}" --version 2>/dev/null || true)" + if [[ -n "${OTELCOL_VERSION}" && "${version_output}" != *"${OTELCOL_VERSION}"* ]]; then + echo "[prepare-provision-tools] 目标机 otelcol-contrib 版本不匹配,期望 ${OTELCOL_VERSION},当前: ${version_output:-unknown}" + return 1 + fi + + echo "[prepare-provision-tools] 目标机 otelcol-contrib 已满足要求: ${version_output:-version unknown}" + return 0 +} + +target_spacetime_ready() { + local target_cli="${SPACETIME_ROOT}/bin/current/spacetimedb-cli" + local target_standalone="${SPACETIME_ROOT}/bin/current/spacetimedb-standalone" + local expected_version version_output + + echo "[prepare-provision-tools] 检查目标机 SpacetimeDB: ${SPACETIME_ROOT}/bin/current" + if [[ ! -x "${target_cli}" || ! -x "${target_standalone}" ]]; then + echo "[prepare-provision-tools] 目标机 SpacetimeDB current 目录不完整,将准备交付文件。" + return 1 + fi + + expected_version="$(resolve_spacetime_expected_version)" + version_output="$("${target_cli}" --version 2>/dev/null || true)" + if [[ -n "${expected_version}" && "${version_output}" != *"${expected_version}"* ]]; then + echo "[prepare-provision-tools] 目标机 SpacetimeDB 版本不匹配,期望 ${expected_version},当前: ${version_output:-unknown}" + return 1 + fi + + echo "[prepare-provision-tools] 目标机 SpacetimeDB 已满足要求: ${version_output:-version unknown}" + return 0 +} + validate_relative_dir() { local label="$1" local path="$2" @@ -101,13 +158,21 @@ prepare_otelcol() { require_cmd tar + if target_otelcol_ready; then + echo "[prepare-provision-tools] 复用目标机已有 otelcol-contrib: ${OTELCOL_TARGET_BIN}" + install -m 0755 "${OTELCOL_TARGET_BIN}" "${target}" + "${target}" --version >/dev/null + OTELCOL_SOURCE_DESCRIPTION="target existing ${OTELCOL_TARGET_BIN}" + return + fi + if [[ -n "${OTELCOL_ARCHIVE_PATH}" && -f "${OTELCOL_ARCHIVE_PATH}" ]]; then source_archive="${OTELCOL_ARCHIVE_PATH}" elif [[ -n "${PROVISION_DOWNLOADS_DIR}" && -f "${downloaded_archive}" ]]; then source_archive="${downloaded_archive}" fi if [[ "${PROVISION_REQUIRE_LOCAL_DOWNLOADS}" == "true" && -z "${source_archive}" ]]; then - echo "[prepare-provision-tools] 要求使用 Windows 已下载的 otelcol-contrib 包,但未找到: ${downloaded_archive}" >&2 + echo "[prepare-provision-tools] 要求使用本地已有的 otelcol-contrib 来源,但目标机未满足且未找到下载包: ${downloaded_archive}" >&2 exit 1 fi @@ -146,6 +211,17 @@ prepare_spacetime() { local downloaded_installer="${PROVISION_DOWNLOADS_DIR}/spacetime-install.sh" local source_installer="" + if target_spacetime_ready; then + echo "[prepare-provision-tools] 复用目标机已有 SpacetimeDB 安装: ${SPACETIME_ROOT}/bin/current" + mkdir -p "${target_dir}" + cp -a "${SPACETIME_ROOT}/bin" "${target_dir}/bin" + chmod 0755 "${target_dir}/bin/current/spacetimedb-cli" "${target_dir}/bin/current/spacetimedb-standalone" + make_spacetime_wrapper "${target_dir}/spacetime" + "${target_dir}/spacetime" --version >/dev/null + SPACETIME_SOURCE_DESCRIPTION="target existing ${SPACETIME_ROOT}/bin/current" + return + fi + mkdir -p "${install_root}" if [[ -n "${SPACETIME_ARCHIVE_PATH}" && -f "${SPACETIME_ARCHIVE_PATH}" ]]; then source_archive="${SPACETIME_ARCHIVE_PATH}" @@ -165,7 +241,7 @@ prepare_spacetime() { source_update="${downloaded_update}" fi if [[ "${PROVISION_REQUIRE_LOCAL_DOWNLOADS}" == "true" && -z "${source_archive}" ]]; then - echo "[prepare-provision-tools] 要求使用 Windows 已下载的 SpacetimeDB release tarball,但未找到: ${downloaded_archive}" >&2 + echo "[prepare-provision-tools] 要求使用本地已有的 SpacetimeDB release tarball,但目标机未满足且未找到下载包: ${downloaded_archive}" >&2 exit 1 fi @@ -185,7 +261,7 @@ prepare_spacetime() { fi if [[ "${PROVISION_REQUIRE_LOCAL_DOWNLOADS}" == "true" && -z "${source_installer}" ]]; then - echo "[prepare-provision-tools] 要求使用 Windows 已下载的 SpacetimeDB 官方安装器脚本,但未找到: ${downloaded_installer}" >&2 + echo "[prepare-provision-tools] 要求使用本地已有的 SpacetimeDB 官方安装器脚本,但未找到: ${downloaded_installer}" >&2 exit 1 elif [[ -n "${source_installer}" ]]; then echo "[prepare-provision-tools] 使用已下载的 SpacetimeDB 官方安装器脚本: ${source_installer}"