Merge branch 'codex/container-simulate'

# Conflicts:
#	.hermes/shared-memory/decision-log.md
#	server-rs/crates/api-server/src/puzzle.rs
#	server-rs/crates/spacetime-client/src/mapper.rs
This commit is contained in:
kdletters
2026-05-19 10:07:45 +08:00
40 changed files with 2795 additions and 165 deletions

View File

@@ -89,7 +89,7 @@ function printHelp(isError) {
Commands:
container:init 生成 deploy/container/api-server.env
container:build 构建 api-server 容器镜像
container:up 后台启动 api-server + nginx + otelcol
container:up 后台启动 spacetimedb + api-server + nginx + otelcol
container:down 停止并清理容器
container:logs 查看容器日志
container:ps 查看容器状态

View File

@@ -1,6 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
PROVISION_TOOLS_DIR="${PROVISION_TOOLS_DIR:-provision-tools}"
SPACETIME_BIN_SOURCE="${SPACETIME_BIN_SOURCE:-${PROVISION_TOOLS_DIR}/spacetime/spacetime}"
OTELCOL_BIN_SOURCE="${OTELCOL_BIN_SOURCE:-${PROVISION_TOOLS_DIR}/otelcol-contrib}"
require_non_root_relative_path() {
local label="$1"
local path="$2"
if [[ -z "${path}" ]]; then
echo "[server-provision] ${label} 不能为空。" >&2
exit 1
fi
if [[ "${path}" == /* || "${path}" == *..* ]]; then
echo "[server-provision] ${label} 只能是工作区内的相对路径: ${path}" >&2
exit 1
fi
}
require_path() {
local path="$1"
if [[ ! -e "${path}" ]]; then
@@ -90,16 +108,16 @@ install_sccache() {
fi
echo "[server-provision] 未找到 sccache准备通过 cargo install sccache 安装。"
if ! command -v cargo >/dev/null 2>&1; then
echo "[server-provision] 未找到 cargo无法自动安装 sccache。请先安装 Rust 工具链后重跑 Server-Provision。" >&2
exit 1
fi
if [[ "${DRY_RUN}" == "true" ]]; then
echo "+ cargo install sccache --locked"
return
fi
if ! command -v cargo >/dev/null 2>&1; then
echo "[server-provision] 未找到 cargo无法自动安装 sccache。请先安装 Rust 工具链后重跑 Server-Provision。" >&2
exit 1
fi
cargo install sccache --locked
if ! command -v sccache >/dev/null 2>&1 && [[ ! -x /root/.cargo/bin/sccache ]]; then
echo "[server-provision] sccache 安装后仍不可用,请检查 cargo bin 目录是否在 PATH 中。" >&2
@@ -107,6 +125,42 @@ install_sccache() {
fi
}
sync_otelcol_install() {
local target_bin="/usr/local/bin/otelcol-contrib"
local source_bin="${OTELCOL_BIN_SOURCE}"
local version="${OTELCOL_VERSION:-0.151.0}"
local resolved_source="${source_bin}"
if [[ "${ENABLE_OTELCOL:-true}" != "true" ]]; then
echo "[server-provision] ENABLE_OTELCOL=${ENABLE_OTELCOL:-},跳过 otelcol-contrib 配置。"
return
fi
if command -v readlink >/dev/null 2>&1; then
resolved_source="$(readlink -f "${source_bin}" 2>/dev/null || echo "${source_bin}")"
fi
if [[ ! -x "${resolved_source}" ]]; then
echo "[server-provision] otelcol-contrib 不存在或不可执行: ${source_bin}" >&2
echo "[server-provision] 请先在构建机准备好 otelcol-contrib ${version},再通过 provision-tools 上传到目标机。" >&2
exit 1
fi
if [[ "${DRY_RUN}" == "true" ]]; then
echo "+ install -m 0755 ${resolved_source} ${target_bin}"
return
fi
install -m 0755 "${resolved_source}" "${target_bin}"
if ! "${target_bin}" --version >/dev/null 2>&1; then
echo "[server-provision] otelcol-contrib 安装后无法执行: ${target_bin}" >&2
exit 1
fi
if ! "${target_bin}" --version 2>/dev/null | grep -q "${version}"; then
echo "[server-provision] 警告: otelcol-contrib 版本不是期望的 ${version}: $("${target_bin}" --version 2>/dev/null || true)" >&2
fi
}
sync_spacetime_install() {
local root_dir="$1"
local target_bin_dir="${root_dir}/bin/current"
@@ -115,14 +169,6 @@ sync_spacetime_install() {
local resolved_command="${SPACETIME_BIN_SOURCE}"
local install_dir=""
local root_bin="${root_dir}/bin"
local share_bin_dir=""
local version_dir=""
local parent_dir=""
if [[ -x "${target_cli}" && -x "${target_standalone}" ]]; then
echo "[server-provision] SpacetimeDB current 目录已存在: ${target_bin_dir}"
return
fi
echo "[server-provision] 同步 SpacetimeDB current 目录到 ${target_bin_dir}"
if [[ "${DRY_RUN}" == "true" ]]; then
@@ -137,26 +183,10 @@ sync_spacetime_install() {
install_dir="$(cd -- "$(dirname -- "${resolved_command}")" && pwd)"
mkdir -p "${root_bin}"
for share_bin_dir in \
"/usr/.local/share/spacetime/bin" \
"/root/.local/share/spacetime/bin" \
"${HOME:-}/.local/share/spacetime/bin"; do
if [[ -d "${share_bin_dir}" ]]; then
version_dir="$(find "${share_bin_dir}" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -n 1)"
if [[ -n "${version_dir}" && -x "${version_dir}/spacetimedb-cli" && -x "${version_dir}/spacetimedb-standalone" ]]; then
echo "[server-provision] 同步 SpacetimeDB 安装: ${version_dir} -> ${target_bin_dir}"
rm -rf "${target_bin_dir}"
mkdir -p "${target_bin_dir}"
cp -a "${version_dir}/." "${target_bin_dir}/"
chmod +x "${target_cli}" "${target_standalone}"
chown -R spacetimedb:spacetimedb "${root_bin}"
return
fi
fi
done
if [[ -d "${install_dir}/bin" ]]; then
echo "[server-provision] 同步 SpacetimeDB 安装: ${install_dir}/bin -> ${root_bin}"
rm -rf "${root_bin}"
mkdir -p "${root_bin}"
cp -a "${install_dir}/bin/." "${root_bin}/"
elif [[ -x "${install_dir}/spacetimedb-cli" && -x "${install_dir}/spacetimedb-standalone" ]]; then
echo "[server-provision] 同步 SpacetimeDB 安装: ${install_dir} -> ${target_bin_dir}"
@@ -165,14 +195,8 @@ sync_spacetime_install() {
cp -f "${install_dir}/spacetimedb-cli" "${target_cli}"
cp -f "${install_dir}/spacetimedb-standalone" "${target_standalone}"
chmod +x "${target_cli}" "${target_standalone}"
elif [[ -f "${resolved_command}" ]]; then
parent_dir="$(cd -- "${install_dir}/.." && pwd)"
if [[ -d "${parent_dir}/bin" && -x "${parent_dir}/bin/current/spacetimedb-cli" && -x "${parent_dir}/bin/current/spacetimedb-standalone" ]]; then
echo "[server-provision] 同步 SpacetimeDB 安装: ${parent_dir}/bin -> ${root_bin}"
cp -a "${parent_dir}/bin/." "${root_bin}/"
else
echo "[server-provision] 未能从 spacetime 命令路径推断完整 SpacetimeDB 安装目录: ${resolved_command}" >&2
fi
else
echo "[server-provision] 未能从 SpacetimeDB 交付包推断完整安装目录: ${resolved_command}" >&2
fi
if [[ ! -x "${target_cli}" || ! -x "${target_standalone}" ]]; then
@@ -396,6 +420,10 @@ render_api_env_example() {
deploy/env/api-server.env.example
}
render_otelcol_service() {
cat deploy/systemd/otelcol-contrib.service
}
validate_nginx_tls() {
local cert_dir="/etc/letsencrypt/live/${SERVER_NAME}"
if [[ "${SERVER_NAME}" == "genarrative.example.com" ]]; then
@@ -532,6 +560,8 @@ render_api_service() {
require_path deploy/systemd/spacetimedb.service
require_path deploy/systemd/genarrative-api.service
require_path deploy/systemd/otelcol-contrib.service
require_path deploy/otelcol/genarrative-debug.yaml
require_path deploy/nginx/genarrative.conf
require_path deploy/nginx/genarrative-dev-http.conf
require_path deploy/nginx/snippets/genarrative-maintenance.conf
@@ -541,6 +571,7 @@ require_path scripts/deploy/maintenance-off.sh
require_path scripts/deploy/maintenance-status.sh
validate_server_names
require_non_root_relative_path "PROVISION_TOOLS_DIR" "${PROVISION_TOOLS_DIR}"
echo "[server-provision] target=${DEPLOY_TARGET}, dry_run=${DRY_RUN}, nginx_config_mode=${NGINX_CONFIG_MODE}, source_commit=$(cat .jenkins-source-commit)"
@@ -548,7 +579,7 @@ run_cmd id
install_build_dependencies
install_nginx_brotli_modules
install_sccache
run_cmd mkdir -p "${SPACETIME_ROOT}" "${RELEASE_ROOT}" "$(dirname "${CURRENT_LINK}")" "$(dirname "${WEB_LINK}")" /etc/genarrative /var/lib/genarrative/maintenance /var/lib/genarrative/auth
run_cmd mkdir -p "${SPACETIME_ROOT}" "${RELEASE_ROOT}" "$(dirname "${CURRENT_LINK}")" "$(dirname "${WEB_LINK}")" /etc/genarrative /var/lib/genarrative/maintenance /var/lib/genarrative/auth /var/lib/genarrative/tracking-outbox
if ! id spacetimedb >/dev/null 2>&1; then
run_cmd useradd --system --home-dir "${SPACETIME_ROOT}" --shell /usr/sbin/nologin spacetimedb
@@ -595,6 +626,16 @@ else
echo "[server-provision] 已存在环境文件,保留不覆盖: ${API_ENV_FILE}"
fi
if [[ "${ENABLE_OTELCOL:-true}" == "true" ]]; then
sync_otelcol_install
otelcol_service="$(mktemp)"
render_otelcol_service >"${otelcol_service}"
install_file "${otelcol_service}" /etc/systemd/system/otelcol-contrib.service 0644
rm -f "${otelcol_service}"
else
echo "[server-provision] ENABLE_OTELCOL=${ENABLE_OTELCOL:-},跳过 otelcol-contrib service 安装。"
fi
if [[ "${NGINX_CONFIG_MODE}" != "none" ]]; then
install_nginx_config_with_rollback
else
@@ -603,7 +644,13 @@ fi
run_cmd systemctl daemon-reload
if [[ "${ENABLE_SERVICES}" == "true" ]]; then
if [[ "${ENABLE_OTELCOL:-true}" == "true" ]]; then
run_cmd systemctl enable otelcol-contrib.service
fi
run_cmd systemctl enable spacetimedb.service genarrative-api.service
if [[ "${ENABLE_OTELCOL:-true}" == "true" ]]; then
run_cmd systemctl restart otelcol-contrib.service
fi
run_cmd systemctl restart spacetimedb.service
wait_for_spacetimedb_service
ensure_spacetime_owner_client_token

View File

@@ -226,7 +226,7 @@ npm run loadtest:k6:works
## 排障
- 如果公开 gallery 返回 `creation_entry_disabled` 或 503检查本地 creation entry 配置是否禁用了对应入口。
- 如果高压下返回 429优先确认目标环境是否设置了 `GENARRATIVE_API_MAX_CONCURRENT_REQUESTS`。429 表示 api-server 应用层背压已生效不等同于业务错误继续看内存、p95、`http_req_failed` 和 OTLP / Nginx timing 判断阈值是否偏低。
- 如果高压下返回 429优先确认目标环境是否设置了 `GENARRATIVE_API_MAX_CONCURRENT_REQUESTS` 以及 `GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS``GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS``GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS`。429 表示 Nginx 或 api-server 背压已生效不等同于业务错误继续看内存、p95、`http_req_failed` 和 OTLP / Nginx timing 判断阈值是否偏低。
- 如果直连 `api-server` 压测出现 `connection refused` 或 status 0说明压力已经打到 TCP 监听 / accept 层;此时同时检查 `GENARRATIVE_API_LISTEN_BACKLOG`、Nginx upstream keepalive 和是否需要在 Nginx 前置限流,不能只靠应用层背压解释。
- 如果个人作品列表返回 401确认 `AUTH_TOKEN` 是当前 api-server 可识别的 access token。
- 如果详情全部 404确认是否已向目标环境导入与 `WORKS_DATA` 一致的数据。
@@ -247,7 +247,7 @@ sudo journalctl -u genarrative-api.service -f
sudo journalctl -u spacetimedb.service -f
```
api-server 的 OpenTelemetry 默认关闭。需要验证 OTLP traces / metrics / logs 时,先在服务器本机启动只监听 `127.0.0.1``otelcol-contrib` debug exporter
api-server 的 OpenTelemetry 在生产与容器模板里默认开启。需要临时关闭时,显式把 `GENARRATIVE_OTEL_ENABLED=false`需要验证 OTLP traces / metrics / logs 时,先在服务器本机启动只监听 `127.0.0.1``otelcol-contrib` debug exporter
```bash
npm run otel:debug
@@ -317,12 +317,14 @@ Rider 的 Logs 面板展示的是 OTLP log event 自身字段,不会自动把
- `process.memory.usage`:进程常驻内存 / RSS。
- `process.memory.virtual`进程虚拟内存Windows 当前按 `PrivateUsage` 上报Linux 取 `VmSize`
- `genarrative.process.memory.private`进程私有内存Windows 来自 `PrivateUsage`Linux 近似取 `/proc/self/status``VmData`
- `process.cpu.time`:进程 user + system 累计 CPU 秒数。
- `genarrative.process.cpu.usage_percent`:两次指标采集之间的进程 CPU 使用率100% 约等于占满 1 个 CPU core。
- `process.thread.count`:线程数。
- `process.windows.handle.count`Windows 句柄数。
- `process.unix.file_descriptor.count`Linux 文件描述符数。
- `genarrative.http.server.response_bodies.in_flight`Axum / Hyper 仍持有的响应 body 数;如果内存高但该值很低,说明热点不在业务 handler 生命周期内。
- `genarrative.http.server.request_permits.available`:应用层 HTTP 背压剩余 permit 数;如果该值未接近 0说明没有打满 `GENARRATIVE_API_MAX_CONCURRENT_REQUESTS`
- `genarrative.puzzle_gallery.cache.hits` / `genarrative.puzzle_gallery.cache.misses` / `genarrative.puzzle_gallery.cache.rebuilds`:拼图广场响应缓存命中、未命中和重建次数。
- `genarrative.http.server.request_permits.available`:应用层 HTTP 背压剩余 permit 数,带 `pool=default|gallery|detail|admin`;如果目标 pool 未接近 0说明没有打满对应 `GENARRATIVE_API_*_MAX_CONCURRENT_REQUESTS`
- `genarrative.puzzle_gallery.cache.hits` / `genarrative.puzzle_gallery.cache.stale_hits` / `genarrative.puzzle_gallery.cache.misses` / `genarrative.puzzle_gallery.cache.refreshes_started` / `genarrative.puzzle_gallery.cache.refreshes_failed` / `genarrative.puzzle_gallery.cache.rebuilds`:拼图广场响应缓存 fresh 命中、stale 命中、未命中、后台刷新和重建次数。
- `genarrative.puzzle_gallery.cache.rebuild.duration`:拼图广场缓存重建耗时。
- `genarrative.puzzle_gallery.cache.data_json_bytes`:拼图广场缓存内预序列化 data JSON 大小。
- `genarrative.spacetime.read.calls` / `genarrative.spacetime.read.duration_ms`SpacetimeDB 订阅本地 cache 读次数和耗时;`read=list_puzzle_gallery` 表示当前路径走 view / local cache不是 procedure。
@@ -336,7 +338,7 @@ Rider 的 Logs 面板展示的是 OTLP log event 自身字段,不会自动把
```bash
systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax
cat /proc/$(pidof api-server)/limits
tr '\0' '\n' < /proc/$(pidof api-server)/environ | grep GENARRATIVE_API_MAX_CONCURRENT_REQUESTS
tr '\0' '\n' < /proc/$(pidof api-server)/environ | grep 'GENARRATIVE_API_.*MAX_CONCURRENT_REQUESTS'
ss -ltnp | grep 8082
curl -sS http://127.0.0.1:8082/healthz
```

View File

@@ -1,10 +1,12 @@
{
"source": "spacetime-migration-7.local.json",
"generatedAt": "2026-05-11T13:09:51.569Z",
"source": "spacetime-migration-1.json",
"generatedAt": "2026-05-18T11:54:04.280Z",
"counts": {
"puzzle_work_profile": 3,
"custom_world_profile": 1,
"match3d_work_profile": 0
"match3d_work_profile": 0,
"square_hole_work_profile": 0,
"visual_novel_work_profile": 0
},
"tables": {
"puzzle_work_profile": [
@@ -113,7 +115,9 @@
}
}
],
"match3d_work_profile": []
"match3d_work_profile": [],
"square_hole_work_profile": [],
"visual_novel_work_profile": []
},
"profileIds": {
"puzzle": [

View File

@@ -137,12 +137,12 @@ function unwrapPayload(json) {
}
function hasCollection(payload, keys) {
return keys.some((key) => Array.isArray(payload?.[key]));
return Boolean(payload) && keys.some((key) => Array.isArray(payload[key]));
}
function firstCollection(payload, keys) {
for (const key of keys) {
if (Array.isArray(payload?.[key])) return payload[key];
if (payload && Array.isArray(payload[key])) return payload[key];
}
return [];
}
@@ -152,10 +152,11 @@ function hasListItemShape(payload, keys) {
if (collection.length === 0) return true;
const item = collection[0];
const hasId = Boolean(
item?.profileId || item?.profile_id || item?.workId || item?.work_id || item?.publicWorkCode,
item &&
(item.profileId || item.profile_id || item.workId || item.work_id || item.publicWorkCode),
);
const hasTitle = Boolean(
item?.title || item?.workTitle || item?.work_title || item?.levelName || item?.worldName,
item && (item.title || item.workTitle || item.work_title || item.levelName || item.worldName),
);
return hasId && hasTitle;
}
@@ -213,7 +214,8 @@ function performDetailRequest() {
const payload = unwrapPayload(json);
const ok = check(response, {
[`${endpoint.name} status is 200`]: (res) => res.status === 200,
[`${endpoint.name} has detail payload`]: () => endpoint.expectKeys.some((key) => payload?.[key]),
[`${endpoint.name} has detail payload`]: () =>
Boolean(payload) && endpoint.expectKeys.some((key) => payload[key]),
});
worksDetailShapeErrorRate.add(!ok, { endpoint: endpoint.name });
}

View File

@@ -0,0 +1,132 @@
#!/usr/bin/env bash
set -euo pipefail
PROVISION_TOOLS_DIR="${PROVISION_TOOLS_DIR:-provision-tools}"
OTELCOL_VERSION="${OTELCOL_VERSION:-0.151.0}"
OTELCOL_DOWNLOAD_ROOT="${OTELCOL_DOWNLOAD_ROOT:-https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download}"
SPACETIME_INSTALLER_URL="${SPACETIME_INSTALLER_URL:-https://install.spacetimedb.com}"
SPACETIME_DOWNLOAD_ROOT="${SPACETIME_DOWNLOAD_ROOT:-https://github.com/clockworklabs/SpacetimeDB/releases/latest/download}"
PROVISION_TOOLS_TMP_PARENT="${PROVISION_TOOLS_TMP_PARENT:-${WORKSPACE:-$(pwd)}/.tmp/server-provision-tools}"
TMP_DIR_TO_CLEAN=""
cleanup_tmp_dir() {
if [[ -n "${TMP_DIR_TO_CLEAN}" ]]; then
rm -rf "${TMP_DIR_TO_CLEAN}"
fi
}
require_cmd() {
local name="$1"
if ! command -v "${name}" >/dev/null 2>&1; then
echo "[prepare-provision-tools] 缺少命令: ${name}" >&2
exit 1
fi
}
download_file() {
local url="$1"
local output="$2"
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 3 --retry-delay 2 "${url}" -o "${output}"
elif command -v wget >/dev/null 2>&1; then
wget -O "${output}" "${url}"
else
echo "[prepare-provision-tools] 需要 curl 或 wget 下载: ${url}" >&2
exit 1
fi
}
make_spacetime_wrapper() {
local target="$1"
cat >"${target}" <<'EOF'
#!/usr/bin/env sh
set -eu
SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
exec "$SELF_DIR/bin/current/spacetimedb-cli" "$@"
EOF
chmod 0755 "${target}"
}
prepare_otelcol() {
local tmp_dir="$1"
local archive="${tmp_dir}/otelcol-contrib.tar.gz"
local extract_dir="${tmp_dir}/otelcol-contrib"
local url="${OTELCOL_DOWNLOAD_ROOT}/v${OTELCOL_VERSION}/otelcol-contrib_${OTELCOL_VERSION}_linux_amd64.tar.gz"
local target="${PROVISION_TOOLS_DIR}/otelcol-contrib"
require_cmd tar
echo "[prepare-provision-tools] 下载 otelcol-contrib: ${url}"
mkdir -p "${extract_dir}"
download_file "${url}" "${archive}"
tar -xzf "${archive}" -C "${extract_dir}"
if [[ ! -x "${extract_dir}/otelcol-contrib" ]]; then
echo "[prepare-provision-tools] otelcol-contrib 包中缺少可执行文件。" >&2
exit 1
fi
install -m 0755 "${extract_dir}/otelcol-contrib" "${target}"
"${target}" --version >/dev/null
}
prepare_spacetime() {
local tmp_dir="$1"
local install_root="${tmp_dir}/spacetime-root"
local target_dir="${PROVISION_TOOLS_DIR}/spacetime"
echo "[prepare-provision-tools] 使用官方安装器准备 SpacetimeDB: ${SPACETIME_INSTALLER_URL}"
mkdir -p "${install_root}"
download_file "${SPACETIME_INSTALLER_URL}" "${tmp_dir}/spacetime-install.sh"
chmod 0755 "${tmp_dir}/spacetime-install.sh"
TMPDIR="${tmp_dir}" SPACETIME_DOWNLOAD_ROOT="${SPACETIME_DOWNLOAD_ROOT}" sh "${tmp_dir}/spacetime-install.sh" --root-dir "${install_root}" -y
if [[ ! -x "${install_root}/bin/current/spacetimedb-cli" ]]; then
echo "[prepare-provision-tools] SpacetimeDB 安装结果缺少 bin/current/spacetimedb-cli。" >&2
exit 1
fi
if [[ ! -x "${install_root}/bin/current/spacetimedb-standalone" ]]; then
echo "[prepare-provision-tools] SpacetimeDB 安装结果缺少 bin/current/spacetimedb-standalone。" >&2
exit 1
fi
mkdir -p "${target_dir}"
cp -a "${install_root}/bin" "${target_dir}/bin"
make_spacetime_wrapper "${target_dir}/spacetime"
"${target_dir}/spacetime" --version >/dev/null
}
main() {
local tmp_dir
require_cmd chmod
require_cmd cp
require_cmd install
require_cmd mktemp
require_cmd rm
mkdir -p "${PROVISION_TOOLS_TMP_PARENT}"
tmp_dir="$(mktemp -d "${PROVISION_TOOLS_TMP_PARENT%/}/run.XXXXXX")"
TMP_DIR_TO_CLEAN="${tmp_dir}"
trap cleanup_tmp_dir EXIT
rm -rf "${PROVISION_TOOLS_DIR}"
mkdir -p "${PROVISION_TOOLS_DIR}"
prepare_otelcol "${tmp_dir}"
prepare_spacetime "${tmp_dir}"
cat >"${PROVISION_TOOLS_DIR}/MANIFEST.txt" <<EOF
otelcol-contrib ${OTELCOL_VERSION}
spacetime installer ${SPACETIME_INSTALLER_URL}
spacetime download root ${SPACETIME_DOWNLOAD_ROOT}
EOF
echo "[prepare-provision-tools] 工具包已准备: ${PROVISION_TOOLS_DIR}"
find "${PROVISION_TOOLS_DIR}" -maxdepth 5 \( -type f -o -type l \) | sort
}
main "$@"