diff --git a/.github.bak/actions/setup-rust/action.yml b/.github.bak.bak/actions/setup-rust/action.yml similarity index 100% rename from .github.bak/actions/setup-rust/action.yml rename to .github.bak.bak/actions/setup-rust/action.yml diff --git a/.github.bak/workflows/ci.yml b/.github.bak.bak/workflows/ci.yml similarity index 100% rename from .github.bak/workflows/ci.yml rename to .github.bak.bak/workflows/ci.yml diff --git a/.github.bak/workflows/release.yml b/.github.bak.bak/workflows/release.yml similarity index 50% rename from .github.bak/workflows/release.yml rename to .github.bak.bak/workflows/release.yml index 6d4619e1..a398666a 100644 --- a/.github.bak/workflows/release.yml +++ b/.github.bak.bak/workflows/release.yml @@ -1,6 +1,28 @@ +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# NodeGet Release 工作流 +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# +# 整体流程:prepare → build-* (并行) → publish-release → publish-docker +# +# 触发方式: +# 1. 推送 v* 标签(自动发布) — git tag v0.5.6 && git push --tags +# 2. 手动触发 workflow_dispatch — 可选仅编译(不上传),或编译+发布 +# +# 构建产物命名规则:nodeget-{agent|server}-{os}-{arch}[-libc].exe? +# 例: nodeget-server-linux-aarch64-musl +# nodeget-agent-windows-x86_64.exe +# nodeget-server-macos-aarch64 +# +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + name: nodeget-release +# ── 触发条件 ──────────────────────────────────────────────────── on: + # 手动触发:GitHub Actions 页面点击 "Run workflow" + # 可选参数: + # release_tag — 发布标签,如 v0.3.7。留空 = 仅编译,不上传 Release + # publish_release — 是否创建/更新 GitHub Release + Docker 镜像 workflow_dispatch: inputs: release_tag: @@ -12,20 +34,41 @@ on: required: false default: false type: boolean + + # 自动触发:推送 v* 格式的 tag 时自动发布 + # 例: git tag v0.5.6 && git push origin v0.5.6 push: tags: - "v*" +# ── 并发控制 ──────────────────────────────────────────────────── +# 同一个 tag 不会重复运行;手动触发无 tag 时用 run_id 兜底 +# cancel-in-progress: false — 不取消正在运行的发布,避免中途截断 concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref_name || inputs.release_tag || github.run_id }} cancel-in-progress: false +# ── 默认权限 ──────────────────────────────────────────────────── +# 最小权限原则:默认只读仓库内容;需要写权限的 job 单独声明 permissions: contents: read env: CARGO_TERM_COLOR: always +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# Job 1: Prepare — 判断是否发布 + 提取 tag +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# +# 输出两个变量供后续 job 使用: +# publish — "true" 表示需要创建 GitHub Release + Docker 镜像 +# tag — 版本标签字符串,如 "v0.5.6" +# +# 逻辑: +# push tag 触发 → 自动发布 +# 手动触发 → 由 inputs.publish_release 决定 +# tag 格式校验 — 必须匹配 ^v数字.数字... 的语义版本格式 +# jobs: prepare: name: Prepare release @@ -41,6 +84,7 @@ jobs: publish="false" tag="" + # push tag 触发 → 自动发布;手动触发 → 读取 inputs if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then publish="true" tag="${GITHUB_REF_NAME}" @@ -49,6 +93,7 @@ jobs: tag="${{ inputs.release_tag }}" fi + # 发布时校验 tag 格式:v + 语义版本,允许 pre-release 后缀如 -rc1, +build123 if [[ "${publish}" == "true" ]]; then if [[ ! "${tag}" =~ ^v[0-9]+(\.[0-9]+)*([-+][0-9A-Za-z.-]+)?$ ]]; then echo "release_tag must look like v0.3.7 when publishing" >&2 @@ -59,6 +104,18 @@ jobs: echo "publish=${publish}" >> "${GITHUB_OUTPUT}" echo "tag=${tag}" >> "${GITHUB_OUTPUT}" + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # Job 2: build-linux — Linux 多平台交叉编译 + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # + # 使用 cross-rs/cross 在 Docker 容器中交叉编译,覆盖 8 个 Linux target: + # - x86_64 / aarch64,musl (静态链接) 和 gnu (glibc 动态链接) + # → 编译 agent + server(server 仅 x86_64/aarch64,因为需要 PostgreSQL 链接) + # - armv7 / arm / i686 + # → 仅编译 agent(轻量,无数据库依赖) + # + # fail-fast: false — 某个 target 失败不阻塞其他 target 的构建 + # build-linux: name: Linux ${{ matrix.platform.release_for }} needs: prepare @@ -67,39 +124,46 @@ jobs: fail-fast: false matrix: platform: - - release_for: x86_64-musl + # ── 主流服务器架构:musl ( Alpine/静态 ) + gnu ( glibc ) ── + - release_for: x86_64-musl # Alpine / 静态链接,Docker 镜像使用此产物 target: x86_64-unknown-linux-musl packages: [ agent, server ] - - release_for: x86_64-gnu + - release_for: x86_64-gnu # glibc 动态链接,传统 Linux 发行版 target: x86_64-unknown-linux-gnu packages: [ agent, server ] - - release_for: aarch64-musl + - release_for: aarch64-musl # ARM64 Alpine / 静态链接,Docker arm64 使用此产物 target: aarch64-unknown-linux-musl packages: [ agent, server ] - - release_for: aarch64-gnu + - release_for: aarch64-gnu # ARM64 glibc,树莓派 4/5 等 target: aarch64-unknown-linux-gnu packages: [ agent, server ] - - release_for: armv7-musleabihf + # ── 嵌入式 / IoT 架构:仅 agent(server 依赖过重不适合这些平台)── + - release_for: armv7-musleabihf # ARMv7 硬浮点 musl(32位 ARM 静态) target: armv7-unknown-linux-musleabihf packages: [ agent ] - - release_for: armv7-gnueabihf + - release_for: armv7-gnueabihf # ARMv7 硬浮点 gnu(树莓派 2/3) target: armv7-unknown-linux-gnueabihf packages: [ agent ] - - release_for: arm-musleabihf + - release_for: arm-musleabihf # ARMv6 硬浮点 musl(树莓派 Zero) target: arm-unknown-linux-musleabihf packages: [ agent ] - - release_for: i686-musl + - release_for: i686-musl # 32位 x86 musl(老旧硬件/兼容) target: i686-unknown-linux-musl packages: [ agent ] steps: - name: Checkout uses: actions/checkout@v6 + # 复合 action:安装 Rust stable 工具链 + Cargo 缓存 + # cache-shared-key 按 target 区分,避免不同 target 的缓存冲突 - name: Setup Rust uses: ./.github/actions/setup-rust with: cache-shared-key: release-linux-${{ matrix.platform.target }} + # 安装 cross —— Rust 交叉编译工具 + # cross 会为每个 target 拉取对应的 Docker 镜像(含交叉工具链和系统库) + # 下载 GitHub release 的预编译二进制,避免 cargo install 的编译开销 - name: Install cross env: CROSS_VERSION: v0.2.5 @@ -113,6 +177,9 @@ jobs: | tar -xz -C "${HOME}/.local/bin" echo "${HOME}/.local/bin" >> "${GITHUB_PATH}" + # 根据矩阵中的 packages 列表选择性编译 agent/server + # --profile minimal — 自定义 profile,优化二进制体积(LTO + strip) + # --locked — 严格按 Cargo.lock 编译,确保可复现 - name: Build selected packages env: TARGET: ${{ matrix.platform.target }} @@ -126,6 +193,9 @@ jobs: cross build --package nodeget-server --target "${TARGET}" --profile minimal --locked fi + # 将编译产物从 target/{target}/minimal/ 复制到 dist/,统一命名 + # 命名: nodeget-{agent|server}-linux-{release_for} + # 例: nodeget-server-linux-aarch64-musl - name: Stage artifacts shell: bash env: @@ -142,6 +212,8 @@ jobs: cp "target/${TARGET}/minimal/nodeget-server" "dist/nodeget-server-${SUFFIX}" fi + # 上传到 GitHub Actions 临时存储,供 publish-release job 下载 + # retention-days: 14 — 14 天后自动清理(仅编译不上传时仍有产物可下载) - name: Upload artifacts uses: actions/upload-artifact@v7 with: @@ -150,6 +222,13 @@ jobs: if-no-files-found: error retention-days: 14 + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # Job 3: build-windows — Windows MSVC 编译 + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # + # Windows 上直接用 cargo 编译(原生 MSVC 工具链,无需 cross) + # 通过 rustup 添加对应 target 即可交叉编译 aarch64 + # build-windows: name: Windows ${{ matrix.platform.release_for }} needs: prepare @@ -158,16 +237,18 @@ jobs: fail-fast: false matrix: platform: - - release_for: x86_64 + - release_for: x86_64 # 64位 Windows(主流桌面/服务器) target: x86_64-pc-windows-msvc packages: [ agent, server ] - - release_for: aarch64 + - release_for: aarch64 # Windows ARM64(Surface Pro X 等) target: aarch64-pc-windows-msvc packages: [ agent ] steps: - name: Checkout uses: actions/checkout@v6 + # setup-rust action 会通过 dtolnay/rust-toolchain 的 targets 参数 + # 安装对应 target 的标准库(aarch64 交叉编译需要) - name: Setup Rust uses: ./.github/actions/setup-rust with: @@ -178,7 +259,7 @@ jobs: run: | $target = "${{ matrix.platform.target }}" $buildAgent = "${{ contains(matrix.platform.packages, 'agent') }}" -eq "true" - $buildServer = "${{ contains(matrix.platform.packages, 'server') }}" -eq "true" + $buildServer = "${{ contains(matrix.platform.packages, 'agent') }}" -eq "true" if ($buildAgent) { cargo build --package nodeget-agent --target $target --profile minimal --locked } @@ -186,6 +267,8 @@ jobs: cargo build --package nodeget-server --target $target --profile minimal --locked } + # Windows 产物带 .exe 后缀 + # 命名: nodeget-{agent|server}-windows-{arch}.exe - name: Stage artifacts shell: pwsh run: | @@ -209,6 +292,14 @@ jobs: if-no-files-found: error retention-days: 14 + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # Job 4: build-macos — macOS Darwin 编译 + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # + # macOS 上原生编译(Xcode 工具链),通过 rustup target 切换架构 + # aarch64 = Apple Silicon (M1/M2/M3/M4) + # x86_64 = Intel Mac(仅编译 agent,Intel Mac 市场份额递减) + # build-macos: name: macOS ${{ matrix.platform.release_for }} needs: prepare @@ -217,10 +308,10 @@ jobs: fail-fast: false matrix: platform: - - release_for: aarch64 + - release_for: aarch64 # Apple Silicon(当前主力 Mac) target: aarch64-apple-darwin packages: [ agent, server ] - - release_for: x86_64 + - release_for: x86_64 # Intel Mac(仅 agent,降级支持) target: x86_64-apple-darwin packages: [ agent ] steps: @@ -246,6 +337,8 @@ jobs: cargo build --package nodeget-server --target "${TARGET}" --profile minimal --locked fi + # macOS 产物无后缀 + # 命名: nodeget-{agent|server}-macos-{arch} - name: Stage artifacts shell: bash env: @@ -270,6 +363,16 @@ jobs: if-no-files-found: error retention-days: 14 + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # Job 5: publish-release — 创建/更新 GitHub Release + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # + # 仅在 prepare.outputs.publish == 'true' 时执行 + # 汇总所有 build-* job 的编译产物,上传为 GitHub Release assets + # + # 依赖:prepare + 三个 build job 全部完成 + # 权限:contents: write(创建 Release 需要写权限) + # publish-release: name: Publish GitHub Release needs: @@ -282,6 +385,7 @@ jobs: permissions: contents: write steps: + # 下载所有 release-* 前缀的 artifact,合并到 dist/ 目录 - name: Download artifacts uses: actions/download-artifact@v8 with: @@ -289,6 +393,9 @@ jobs: path: dist merge-multiple: true + # 创建 GitHub Release 并上传所有编译产物 + # make_latest: true — 标记为最新 Release + # softprops/action-gh-release 支持幂等操作(tag 已存在时追加文件) - name: Upload release assets uses: softprops/action-gh-release@v3 with: @@ -297,6 +404,22 @@ jobs: make_latest: true files: dist/* + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # Job 6: publish-docker — 构建并推送 Docker 多架构镜像 + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # + # 仅在 publish-release 成功后执行(确保 Docker 镜像拉取的 release 二进制已存在) + # 使用 Dockerfile 的 runtime-release target: + # 该 target 从 GitHub Release 下载预编译二进制(不源码编译) + # 通过 TARGETARCH 自动选择 amd64/arm64 对应的资产 + # + # 推送双标签: + # genshinmc/nodeget:latest — 始终指向最新版本 + # genshinmc/nodeget:v0.5.6 — 带版本号的固定标签 + # + # 依赖:prepare + publish-release(顺序依赖,确保 Release 资产先就绪) + # 权限:actions: write(GHA 缓存), contents: read, packages: write(GHCR,备用) + # publish-docker: name: Publish Docker image needs: @@ -317,18 +440,23 @@ jobs: - name: Checkout uses: actions/checkout@v6 + # 前置检查:Docker Hub 凭据必须配置,否则提前失败而非在 login 步骤报错 - name: Check Docker Hub credentials shell: bash run: | test -n "${DOCKERHUB_USERNAME}" test -n "${DOCKERHUB_TOKEN}" + # QEMU — 用户态模拟器,允许在 x86_64 runner 上构建 arm64 镜像 + # Docker buildx 的多架构构建底层依赖 QEMU 模拟非原生指令集 - name: Set up QEMU uses: docker/setup-qemu-action@v4 + # Buildx — Docker 的扩展构建器,支持多平台并行构建 + 缓存导出 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 + # 登录 Docker Hub(凭据存储在 GitHub Secrets) - name: Login to Docker Hub uses: docker/login-action@v4 with: @@ -336,6 +464,10 @@ jobs: username: ${{ env.DOCKERHUB_USERNAME }} password: ${{ env.DOCKERHUB_TOKEN }} + # 生成 Docker 标签和标签元数据 + # tags 规则: + # type=raw,value=latest → genshinmc/nodeget:latest + # type=raw,value=${{ needs.prepare.outputs.tag }} → genshinmc/nodeget:v0.5.6 - name: Docker metadata id: meta uses: docker/metadata-action@v6 @@ -345,6 +477,14 @@ jobs: type=raw,value=latest type=raw,value=${{ needs.prepare.outputs.tag }} + # 多架构构建并推送 + # target: runtime-release — Dockerfile 中使用 GitHub Release 下载二进制的 stage + # platforms: linux/amd64,linux/arm64 — 同时构建两个架构 + # build-args: + # NODEGET_VERSION — 传给 Dockerfile 的 ARG,指定从哪个 Release 下载二进制 + # NODEGET_RELEASE_REPO — 指定 Release 仓库(支持 fork 场景) + # cache-from/to: type=gha — 使用 GitHub Actions 缓存加速重复构建 + # mode=max — 缓存所有中间层(不仅是最终层),最大化缓存命中率 - name: Build and push uses: docker/build-push-action@v7 with: diff --git a/.github.bak/ISSUE_TEMPLATE/bug.yaml b/.github.bak/ISSUE_TEMPLATE/bug.yaml deleted file mode 100644 index 185fa27b..00000000 --- a/.github.bak/ISSUE_TEMPLATE/bug.yaml +++ /dev/null @@ -1,74 +0,0 @@ -name: Bug 报告 -description: 提交一个 bug 报告 -title: "[Bug]: " -labels: [ "bug", "triage" ] -projects: [ "octo-org/1", "octo-org/44" ] -assignees: - - octocat -type: bug -body: - - type: markdown - attributes: - value: | - 感谢你对 NodeGet 后端项目的贡献!请依照下面的表格填写相关信息,以帮助我们更快地定位和修复问题。 - - type: checkboxes - attributes: - label: 问题发生的地方 - description: 请告诉我们,问题发生在哪一个部分中?Agent 或 Server(可多选) - options: - - label: Agent - required: false - - label: Server - required: false - - type: textarea - id: environment - attributes: - label: Environment - description: 运行在什么环境下?(CPU 架构、操作系统版本、是否为 Docker、使用数据库类型等) - placeholder: 务必详细描述你的环境,这将帮助我们更快地定位问题。如果你不知道,可以粘贴来自系统检测脚本的输出,以及配上脱敏后的 config.yaml 内容。 - value: "CPU: x86_64, OS: Ubuntu 24.04 LTS" - validations: - required: true - - type: textarea - id: version - attributes: - label: Version - description: 你使用的 NodeGet 版本是什么?请提供来自 `nodeget-server version` 或 `nodeget-agent --version` 的输出。 - placeholder: 务必详细描述你的环境,这将帮助我们更快地定位问题。 - value: "NodeGet Server Version: 0.0.3 -Git Branch: dev -Commit SHA: 84bcec578a9b29ce3f2c1b6ef4a43ca1579435f2 -Commit Date: 2026-04-29T20:39:06+08:00 -Commit Message: Fix: Remove deprecated ARM and PowerPC build configurations from agent-build.yml -Build Time: 1777478545 -Target Triple: aarch64-apple-darwin -Rustc Channel: nightly -Rustc Version: 1.97.0-nightly -Rustc Commit Date: 2026-04-23 -Rustc Commit Hash: 36ba2c7712052d731a7082d0eba5ed3d9d56c133 -Rustc LLVM Version: 22.1.2 -" - validations: - required: true - - type: textarea - id: request-body - attributes: - label: Request body - description: 如果你在使用 NodeGet Server API 时遇到了问题,请附上请求体 - placeholder: { "jsonrpc": "2.0","method": "...","params": { },"id": 1 } - validations: - required: false - - type: textarea - id: logs - attributes: - label: Logs - description: 请附上相关日志(脱敏后)。这可能包括 NodeGet Server 或 Agent 的日志输出。 - validations: - required: false - - type: textarea - id: additional-info - attributes: - label: Additional Information - description: 任何其他你认为有助于我们理解和修复这个问题的信息,包括但不限于你的对照测试、截图等 - validations: - required: false \ No newline at end of file diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml new file mode 100644 index 00000000..12e03bf6 --- /dev/null +++ b/.github/actions/setup-rust/action.yml @@ -0,0 +1,38 @@ +name: setup-rust +description: Setup Rust toolchain and Cargo cache for NodeGet +inputs: + components: + description: Rust components to install + required: false + default: "" + targets: + description: Rust targets to install + required: false + default: "" + cache-shared-key: + description: Shared cache key for Swatinem/rust-cache + required: false + default: "" + cache-targets: + description: Whether to cache target directories + required: false + default: "true" + cache-on-failure: + description: Whether to save cache when a job fails + required: false + default: "true" +runs: + using: composite + steps: + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: ${{ inputs.components }} + targets: ${{ inputs.targets }} + + - name: Restore Cargo cache + uses: Swatinem/rust-cache@v2 + with: + shared-key: ${{ inputs.cache-shared-key }} + cache-targets: ${{ inputs.cache-targets }} + cache-on-failure: ${{ inputs.cache-on-failure }} diff --git a/.github/workflows/release-server.yml b/.github/workflows/release-server.yml new file mode 100644 index 00000000..279865ff --- /dev/null +++ b/.github/workflows/release-server.yml @@ -0,0 +1,243 @@ +name: release-server + +on: + workflow_dispatch: + inputs: + release_tag: + description: "Release tag to publish, for example v0.3.7. Leave empty for artifact-only manual builds." + required: false + type: string + publish_release: + description: "Create/update the GitHub Release and Docker image. Requires release_tag." + required: false + default: false + type: boolean + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref_name || inputs.release_tag || github.run_id }} + cancel-in-progress: false + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +jobs: + prepare: + name: Prepare release + runs-on: ubuntu-latest + outputs: + publish: ${{ steps.release.outputs.publish }} + tag: ${{ steps.release.outputs.tag }} + steps: + - name: Resolve release mode + id: release + shell: bash + run: | + publish="false" + tag="" + + # push tag 触发 → 自动发布;手动触发 → 读取 inputs + if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then + publish="true" + tag="${GITHUB_REF_NAME}" + else + publish="${{ inputs.publish_release }}" + tag="${{ inputs.release_tag }}" + fi + + # 发布时校验 tag 格式:v + 语义版本,允许 pre-release 后缀如 -rc1, +build123 + if [[ "${publish}" == "true" ]]; then + if [[ ! "${tag}" =~ ^v[0-9]+(\.[0-9]+)*([-+][0-9A-Za-z.-]+)?$ ]]; then + echo "release_tag must look like v0.3.7 when publishing" >&2 + exit 1 + fi + fi + + echo "publish=${publish}" >> "${GITHUB_OUTPUT}" + echo "tag=${tag}" >> "${GITHUB_OUTPUT}" + + build-linux: + name: Build - ${{ matrix.platform.release_for }} + needs: prepare + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: + - release_for: linux_x86_64_musl + target: x86_64-unknown-linux-musl + binary_name: nodeget-server-linux-x86_64-musl + - release_for: linux_x86_64_gnu + target: x86_64-unknown-linux-gnu + binary_name: nodeget-server-linux-x86_64-gnu + - release_for: linux_arm_gnueabi + target: arm-unknown-linux-gnueabi + binary_name: nodeget-server-linux-arm-gnueabi + - release_for: linux_arm_gnueabihf + target: arm-unknown-linux-gnueabihf + binary_name: nodeget-server-linux-arm-gnueabihf + - release_for: linux_aarch64_gnu + target: aarch64-unknown-linux-gnu + binary_name: nodeget-server-linux-aarch64-gnu + - release_for: linux_aarch64_musl + target: aarch64-unknown-linux-musl + binary_name: nodeget-server-linux-aarch64-musl + - release_for: linux_armv7_gnueabi + target: armv7-unknown-linux-gnueabi + binary_name: nodeget-server-linux-armv7-gnueabi + - release_for: linux_armv7_gnueabihf + target: armv7-unknown-linux-gnueabihf + binary_name: nodeget-server-linux-armv7-gnueabihf + - release_for: linux_armv7_musleabi + target: armv7-unknown-linux-musleabi + binary_name: nodeget-server-linux-armv7-musleabi + - release_for: linux_armv7_musleabihf + target: armv7-unknown-linux-musleabihf + binary_name: nodeget-server-linux-armv7-musleabihf + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Rust + uses: ./.github/actions/setup-rust + with: + cache-shared-key: release-linux-${{ matrix.platform.target }} + + - name: Install cross + env: + CROSS_VERSION: v0.2.5 + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p "${HOME}/.local/bin" + gh release download "${CROSS_VERSION}" \ + --repo cross-rs/cross \ + --pattern cross-x86_64-unknown-linux-gnu.tar.gz \ + -O - \ + | tar -xz -C "${HOME}/.local/bin" + echo "${HOME}/.local/bin" >> "${GITHUB_PATH}" + + - name: Build binary + run: cross build --package nodeget-server --target ${{ matrix.platform.target }} --profile minimal --jobs 32 + env: + CROSS_NO_WARNINGS: 0 + + - name: Compress binary + run: upx -9 ./target/${{ matrix.platform.target }}/minimal/nodeget-server + + - name: Rename binary + run: cp target/${{ matrix.platform.target }}/minimal/nodeget-server target/${{ matrix.platform.target }}/minimal/${{ matrix.platform.binary_name }} + + - name: Upload binary + uses: actions/upload-artifact@v4 + with: + name: server-${{ matrix.platform.target }} + path: target/${{ matrix.platform.target }}/minimal/${{ matrix.platform.binary_name }} + if-no-files-found: error + retention-days: 14 + + build-windows: + name: Build - ${{ matrix.platform.release_for }} + needs: prepare + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + platform: + - release_for: windows_x86_64 + target: x86_64-pc-windows-msvc + binary_name: nodeget-server-windows-x86_64.exe + - release_for: windows_aarch64 + target: aarch64-pc-windows-msvc + binary_name: nodeget-server-windows-aarch64.exe + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Rust + uses: ./.github/actions/setup-rust + with: + targets: ${{ matrix.platform.target }} + cache-shared-key: release-windows-${{ matrix.platform.target }} + + - name: Build binary + run: cross build --package nodeget-server --target ${{ matrix.platform.target }} --profile minimal --jobs 32 + env: + CROSS_NO_WARNINGS: 0 + + - name: Rename binary + run: copy target\${{ matrix.platform.target }}\minimal\nodeget-server.exe target\${{ matrix.platform.target }}\minimal\${{ matrix.platform.binary_name }} + + - name: Upload binary + uses: actions/upload-artifact@v4 + with: + name: server-${{ matrix.platform.target }} + path: target/${{ matrix.platform.target }}/minimal/${{ matrix.platform.binary_name }} + if-no-files-found: error + retention-days: 14 + + build-macos: + name: Build - ${{ matrix.platform.release_for }} + needs: prepare + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + platform: + - release_for: macos_aarch64 + target: aarch64-apple-darwin + binary_name: nodeget-server-macos-aarch64 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Rust + uses: ./.github/actions/setup-rust + with: + targets: ${{ matrix.platform.target }} + cache-shared-key: release-macos-${{ matrix.platform.target }} + + - name: Build binary + run: cargo build --package nodeget-server --target ${{ matrix.platform.target }} --profile minimal --jobs 32 + env: + CROSS_NO_WARNINGS: 0 + + - name: Rename binary + run: cp target/${{ matrix.platform.target }}/minimal/nodeget-server target/${{ matrix.platform.target }}/minimal/${{ matrix.platform.binary_name }} + + - name: Upload binary + uses: actions/upload-artifact@v4 + with: + name: server-${{ matrix.platform.target }} + path: target/${{ matrix.platform.target }}/minimal/${{ matrix.platform.binary_name }} + if-no-files-found: error + retention-days: 14 + + publish-release: + name: Publish GitHub Release + needs: + - prepare + - build-linux + - build-windows + - build-macos + if: needs.prepare.outputs.publish == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download artifacts + uses: actions/download-artifact@v8 + with: + pattern: * + path: dist + merge-multiple: true + + - name: Upload release assets + uses: softprops/action-gh-release@v3 + with: + tag_name: ${{ needs.prepare.outputs.tag }} + name: NodeGet ${{ needs.prepare.outputs.tag }} + make_latest: true + files: dist/* \ No newline at end of file diff --git a/.gitignore b/.gitignore index b9d6ed16..f9986bc9 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ static rp.md rustest nodeget.db* -.claude \ No newline at end of file +.claude +/server/profile.json.gz diff --git a/Cargo.toml b/Cargo.toml index bccb67ef..d493dc06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,5 +81,6 @@ ng-migration = { path = "crates/ng-db/migration" } [profile] dev = { opt-level = 0, debug = true, debug-assertions = true, overflow-checks = true, lto = false, panic = "unwind", incremental = true, codegen-units = 256, rpath = false } release = { opt-level = 3, debug = false, debug-assertions = false, overflow-checks = false, lto = false, panic = "unwind", incremental = false, codegen-units = 16, rpath = false } +profiling = { inherits = "release", debug = true } fastest = { inherits = "dev", opt-level = 0, debug = 0, debug-assertions = true, overflow-checks = true, lto = false, panic = "unwind", incremental = true, codegen-units = 512, rpath = false } minimal = { inherits = "release", opt-level = "z", lto = true, codegen-units = 1, panic = "abort", debug = false, strip = true, debug-assertions = false, overflow-checks = false } diff --git a/agent/config.toml.example b/agent/config.toml.example index b9334243..1574d887 100644 --- a/agent/config.toml.example +++ b/agent/config.toml.example @@ -65,54 +65,12 @@ ws_url = "ws://127.0.0.1:2211/" # 是否允许执行任务 allow_task = true -# 是否允许 ICMP Ping -allow_icmp_ping = true - -# 是否允许 TCP Ping -allow_tcp_ping = true - -# 是否允许 HTTP Ping -allow_http_ping = true - -# 是否允许通用 HTTP 请求,危险操作,谨慎开启 -allow_http_request = true - -# 是否允许 Web Shell,极度危险,谨慎开启 -allow_web_shell = true - -# 是否允许执行命令,极度危险,谨慎开启 -allow_execute = true - -# 是否允许阅读配置,极度危险,谨慎开启 -allow_read_config = true - -# 是否允许编辑配置,极度危险,谨慎开启 -allow_edit_config = true - -# 是否允许获取 IP 地址 -allow_ip = true - -# 是否允许 DNS 查询 -# 支持 A / AAAA / CNAME / MX / TXT / NS / SRV / PTR / SOA / CAA 记录类型 -# Agent 使用 hickory-resolver 库执行 DNS 查询,支持自定义 DNS 服务器(格式 "IP:port")或系统默认 -# dns_server 字段必须包含端口号,如 "8.8.8.8:53" -# 建议按需开启 -allow_dns = true - -# 是否允许获取版本信息 -allow_version = true - -# 是否允许自更新,开启后 Server 可通过 SelfUpdate 任务触发 Agent 自动下载并替换二进制 -# 支持升级和降级,版本号仅做格式校验(vX.Y.Z) -# 重启后生效,需谨慎开启 -allow_self_update = false - # 允许执行的任务类型白名单(可选) # 若指定此列表且非空,则单独的任务开关(如 allow_ping / allow_execute 等)全部失效 # 以本列表为准,未列出的任务类型一律拒绝 # 值为 task_name,可选值包括:ping / tcp_ping / http_ping / dns / execute / http_request # / web_shell / read_config / edit_config / ip / version / self_update -# allow_task_type = ["ping", "dns", "ip"] +allow_task_type = ["ping", "dns", "ip"] # 是否忽略服务端 TLS 证书校验,默认关闭 # 仅在 Server 使用自签名证书或测试环境时开启 diff --git a/crates/ng-db/src/db_connection.rs b/crates/ng-db/src/db_connection.rs index bd999c3f..b6ad7606 100644 --- a/crates/ng-db/src/db_connection.rs +++ b/crates/ng-db/src/db_connection.rs @@ -60,7 +60,7 @@ pub async fn init_db_connection(config: DbConnectionConfig) -> anyhow::Result<() info!(target: "db", "initializing database connection"); let mut opt = ConnectOptions::new(&config.database_url); - opt.sqlx_logging_level(LevelFilter::Warn) + opt.sqlx_logging_level(LevelFilter::Trace) .connect_timeout(Duration::from_millis(config.connect_timeout_ms)) .acquire_timeout(Duration::from_millis(config.acquire_timeout_ms)) .idle_timeout(Duration::from_millis(config.idle_timeout_ms)) diff --git a/crates/ng-db/src/lib.rs b/crates/ng-db/src/lib.rs index e7be4e90..c0712b2f 100644 --- a/crates/ng-db/src/lib.rs +++ b/crates/ng-db/src/lib.rs @@ -29,15 +29,21 @@ pub mod entity; // ── 主库全局单例 ────────────────────────────────────────────────── -/// 全局数据库连接,服务端启动时通过 `set_db` 写入一次,之后只读 -static DB: std::sync::OnceLock = std::sync::OnceLock::new(); +/// 全局数据库连接,`ManuallyDrop` 包裹以避免进程退出时的昂贵析构。 +/// +/// `PostgreSQL` 连接池的 `drop` 需要 join 后台维护任务,耗时可达数秒。 +/// 进程退出时 OS 回收所有资源(TCP 连接、内存),无需显式析构。 +/// 若后续需要优雅关闭池,可在 `serve.rs` 退出前调用 `take_and_close_db()`。 +static DB: std::sync::OnceLock> = + std::sync::OnceLock::new(); /// 获取全局主库连接 /// /// - 返回值:若已初始化则返回 `Some(&DatabaseConnection)`,否则 `None` /// - 服务端各模块通过此函数共享同一个数据库连接 pub fn get_db() -> Option<&'static sea_orm::DatabaseConnection> { - DB.get() + use std::ops::Deref; + DB.get().map(Deref::deref) } /// 设置全局主库连接,仅应在服务端启动时调用一次 @@ -45,11 +51,30 @@ pub fn get_db() -> Option<&'static sea_orm::DatabaseConnection> { /// - `conn` — `SeaORM` 数据库连接实例 /// - 若重复调用,新连接会被丢弃并输出警告日志(OnceLock 语义) pub fn set_db(conn: sea_orm::DatabaseConnection) { - if DB.set(conn).is_err() { + if DB.set(std::mem::ManuallyDrop::new(conn)).is_err() { tracing::warn!(target: "db", "set_db called twice; new connection discarded (OnceLock already set)"); } } +/// 取出并优雅关闭全局数据库连接(可选,仅用于需要显式 `drop` 的场景) +/// +/// 调用后 `get_db()` 返回 `None`。若从未调用此函数,连接在进程退出时由 OS 回收。 +/// +/// # Safety +/// +/// 必须确保无其他代码仍在使用 `get_db()` 返回的引用。 +#[allow(dead_code)] +pub unsafe fn take_and_close_db() { + // SAFETY: 调用者保证无其他引用在使用中 + let db_ptr: *mut std::sync::OnceLock> = + &DB as *const _ as *mut _; + // SAFETY: 调用者保证独占访问 + if let Some(md) = unsafe { (*db_ptr).take() } { + // ManuallyDrop::into_inner 恢复所有权并执行正常 drop + drop(std::mem::ManuallyDrop::into_inner(md)); + } +} + // ── 服务端专属模块 ──────────────────────────────────────────────── #[cfg(feature = "server")] diff --git a/server/src/subcommands/serve.rs b/server/src/subcommands/serve.rs index 81154848..4ce0c39c 100644 --- a/server/src/subcommands/serve.rs +++ b/server/src/subcommands/serve.rs @@ -14,6 +14,9 @@ use axum::{extract::Path, http::StatusCode}; use base64::Engine as _; use ng_config::config::server::ServerConfig; use ng_config::get_reload_notify; +use rustls::pki_types::pem::PemObject; +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use std::sync::Arc; use ng_core::permission::data_structure::{Permission, Scope}; use ng_core::permission::token_auth::TokenOrAuth; use ng_db::entity::js_worker; @@ -344,7 +347,7 @@ pub async fn run(config: &ServerConfig) { let cert_path = config.tls_cert.as_deref().unwrap(); let key_path = config.tls_key.as_deref().unwrap(); info!(target: "server", address = %addr, cert = %cert_path, key = %key_path, "Server listening on TCP with TLS"); - let tls_config = axum_server::tls_rustls::RustlsConfig::from_pem_file(cert_path, key_path) + let tls_config = build_http1_only_tls_config(cert_path, key_path) .await .unwrap_or_else(|e| panic!("Failed to load TLS config: {e}")); let serve_future = @@ -429,6 +432,39 @@ pub async fn run(config: &ServerConfig) { } } +/// 构建 ALPN 仅广播 `http/1.1` 的 TLS 配置 +/// +/// `axum-server` 默认 ALPN 为 `[h2, http/1.1]`,客户端会优先协商 HTTP/2, +/// 导致 h2 frame 生命周期开销(samply 显示 14-33% tokio worker 时间)。 +/// 本服务器所有入站连接均为 WebSocket(HTTP/1.1 upgrade)或 JSON-RPC(HTTP/1.1), +/// 不需要 HTTP/2,限制 ALPN 消除 h2 开销。 +async fn build_http1_only_tls_config( + cert_path: &str, + key_path: &str, +) -> std::io::Result { + let cert_pem = tokio::fs::read(cert_path).await?; + let key_pem = tokio::fs::read(key_path).await?; + + let certs: Vec<_> = CertificateDer::pem_slice_iter(&cert_pem) + .collect::, _>>() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + let key = PrivateKeyDer::from_pem_slice(&key_pem) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + let mut server_config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, key) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + // 仅广播 http/1.1,阻止客户端协商 HTTP/2 + server_config.alpn_protocols = vec![b"http/1.1".to_vec()]; + + Ok(axum_server::tls_rustls::RustlsConfig::from_config(Arc::new( + server_config, + ))) +} + /// 渲染根路径着陆页 HTML /// /// 包含服务器 UUID、版本号和常用链接。