commit 41ff2ce04aed748954bd860668b69edd103bba2c Author: 严浩 Date: Tue Aug 12 13:59:27 2025 +0800 ✔ diff --git a/.github/_files/.npmrc b/.github/_files/.npmrc new file mode 100644 index 0000000..83c86a7 --- /dev/null +++ b/.github/_files/.npmrc @@ -0,0 +1 @@ +use-node-version=22.14.0 # https://pnpm.io/zh/npmrc#use-node-version \ No newline at end of file diff --git a/.github/_files/package.json b/.github/_files/package.json new file mode 100644 index 0000000..4619ad8 --- /dev/null +++ b/.github/_files/package.json @@ -0,0 +1,6 @@ +{ + "packageManager": "pnpm@10.6.5", + "dependencies": { + "bun": "^1.2.5" + } +} \ No newline at end of file diff --git a/.github/_files/pnpm-lock.yaml b/.github/_files/pnpm-lock.yaml new file mode 100644 index 0000000..ebdc08d --- /dev/null +++ b/.github/_files/pnpm-lock.yaml @@ -0,0 +1,125 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + bun: + specifier: ^1.2.5 + version: 1.2.5 + +packages: + + '@oven/bun-darwin-aarch64@1.2.5': + resolution: {integrity: sha512-ggZfdpgUJ/OiWrfcfTgHeSTHcec5HAjkGrZHL9FJ/R60sydRKPYHgAgexdIoJAGfsCVAL+x7y8NSTRIAX8J4Ng==} + cpu: [arm64] + os: [darwin] + + '@oven/bun-darwin-x64-baseline@1.2.5': + resolution: {integrity: sha512-3W1RO3/D6Z1S79J47F/DLzmK+dgkYq5hS1ShOCSBAYTTA2b1ZuymaN8avGzSb9ed5W0QfxtyeAksfEY2xUBOqA==} + cpu: [x64] + os: [darwin] + + '@oven/bun-darwin-x64@1.2.5': + resolution: {integrity: sha512-4zqyQLJB33s99KcTxH6yQqH5EYBmF1qofQTtLsToIFbIZN1NqSp/aegYiGmxO5Kj/BuWsy8Wf8MS6vX2O0o2Lw==} + cpu: [x64] + os: [darwin] + + '@oven/bun-linux-aarch64-musl@1.2.5': + resolution: {integrity: sha512-URlISBOE2HQi8qdru691OYywJRwChxMfXFbk26tCgdZ01LgGAKsIjAYylefuSsPuA697imDN3Pel3D7rveusmw==} + cpu: [aarch64] + os: [linux] + + '@oven/bun-linux-aarch64@1.2.5': + resolution: {integrity: sha512-NQFtAVyQyJhLYrhFVxKdh6cqrDNc60pBnBGLQSO8PU+oyFyiJ3e3gGXjLzMbxd6cJxNIK5FZ0JIq96WljKAhlg==} + cpu: [arm64] + os: [linux] + + '@oven/bun-linux-x64-baseline@1.2.5': + resolution: {integrity: sha512-fCm/qp7e3VYlaoRs6NIEsKubPqyxjzLv8/qZkxeLLOlPd7CS8L26UY4KPOSjA+wrhPT+Nxsyvl/EEJq2R/iauA==} + cpu: [x64] + os: [linux] + + '@oven/bun-linux-x64-musl-baseline@1.2.5': + resolution: {integrity: sha512-H7tuJz7mZvOTPo4yLbIXIxkiDGWSGd2DbwGl4zNol/FURqGsKQVqpomv86yl9KCXsUUOm5FX2i5Ed+ro8N//Cg==} + cpu: [x64] + os: [linux] + + '@oven/bun-linux-x64-musl@1.2.5': + resolution: {integrity: sha512-DuU2kQnY48g9tNWjFrZqyG+U2emCBwlhOPxbuY/TMVVNSTMAcQbE/bb3s2pZdhZH5ssjc5SH/ZyWU1TePcYB2A==} + cpu: [x64] + os: [linux] + + '@oven/bun-linux-x64@1.2.5': + resolution: {integrity: sha512-pa3kQ4cXNV0jk5aM8+Hdmxr+b4QoPVgeAIA454SN5l3hMGfNsHjczKpsz0ksInZ8506iMMTCPEBXpyQJcSme+Q==} + cpu: [x64] + os: [linux] + + '@oven/bun-windows-x64-baseline@1.2.5': + resolution: {integrity: sha512-j5FxI8FeKfWI6rEXA+1O3ASBMTp5CFcZ7MR+/aCpiBKrDse32wLaZMVGnvqQqs4y0YHUvR8b7eXHHTboezjL1w==} + cpu: [x64] + os: [win32] + + '@oven/bun-windows-x64@1.2.5': + resolution: {integrity: sha512-oNDdPmzsCyvCATiYgkKWgxOeEx2F7m/i2MGUba+YJAeVXJsJg9iPJrLVBtETvKoSAgkXViwoUEw2U25jRYsp4g==} + cpu: [x64] + os: [win32] + + bun@1.2.5: + resolution: {integrity: sha512-fbQLt+DPiGUrPKdmsHRRT7cQAlfjdxPVFvLZrsUPmKiTdv+pU50ypdx9yRJluknSbyaZchFVV7Lx2KXikXKX2Q==} + cpu: [arm64, x64, aarch64] + os: [darwin, linux, win32] + hasBin: true + +snapshots: + + '@oven/bun-darwin-aarch64@1.2.5': + optional: true + + '@oven/bun-darwin-x64-baseline@1.2.5': + optional: true + + '@oven/bun-darwin-x64@1.2.5': + optional: true + + '@oven/bun-linux-aarch64-musl@1.2.5': + optional: true + + '@oven/bun-linux-aarch64@1.2.5': + optional: true + + '@oven/bun-linux-x64-baseline@1.2.5': + optional: true + + '@oven/bun-linux-x64-musl-baseline@1.2.5': + optional: true + + '@oven/bun-linux-x64-musl@1.2.5': + optional: true + + '@oven/bun-linux-x64@1.2.5': + optional: true + + '@oven/bun-windows-x64-baseline@1.2.5': + optional: true + + '@oven/bun-windows-x64@1.2.5': + optional: true + + bun@1.2.5: + optionalDependencies: + '@oven/bun-darwin-aarch64': 1.2.5 + '@oven/bun-darwin-x64': 1.2.5 + '@oven/bun-darwin-x64-baseline': 1.2.5 + '@oven/bun-linux-aarch64': 1.2.5 + '@oven/bun-linux-aarch64-musl': 1.2.5 + '@oven/bun-linux-x64': 1.2.5 + '@oven/bun-linux-x64-baseline': 1.2.5 + '@oven/bun-linux-x64-musl': 1.2.5 + '@oven/bun-linux-x64-musl-baseline': 1.2.5 + '@oven/bun-windows-x64': 1.2.5 + '@oven/bun-windows-x64-baseline': 1.2.5 diff --git a/.github/gh-packages-delete.sh b/.github/gh-packages-delete.sh new file mode 100644 index 0000000..7bfd6c1 --- /dev/null +++ b/.github/gh-packages-delete.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# 设置变量 +OWNER="yanhao98" +REPO="gemini-balance" +PACKAGE_NAME="gemini-balance" + +# 重新登录 GitHub CLI 获取更多权限 +echo "正在更新 GitHub CLI 权限..." +gh auth refresh -h github.com -s read:packages,delete:packages + +# 列出所有版本的容器镜像 +echo "正在获取 $PACKAGE_NAME 所有版本..." +VERSIONS=$(gh api \ + "/user/packages/container/$PACKAGE_NAME/versions" \ + --paginate \ + --jq '.[].id') + +# 检查是否有版本存在 +if [ -z "$VERSIONS" ]; then + echo "没有找到 $PACKAGE_NAME 的任何版本" + exit 0 +fi + +# 删除每个版本 +echo "开始删除 $PACKAGE_NAME 的所有版本..." +for version_id in $VERSIONS; do + echo "正在删除版本 ID: $version_id" + gh api \ + --method DELETE \ + "/user/packages/container/$PACKAGE_NAME/versions/$version_id" + echo "版本 $version_id 已删除" +done + +echo "所有 $PACKAGE_NAME 容器镜像版本已成功删除" \ No newline at end of file diff --git a/.github/gh-run-delete.sh b/.github/gh-run-delete.sh new file mode 100644 index 0000000..9c474cb --- /dev/null +++ b/.github/gh-run-delete.sh @@ -0,0 +1,4 @@ +while gh run list --json databaseId --jq '.[].databaseId' | grep -q .; do + for id in $(gh run list --json databaseId --jq '.[].databaseId'); do gh run delete $id; done + echo "继续删除下一批..." +done diff --git a/.github/workflows/_delete-workflow-runs.yaml b/.github/workflows/_delete-workflow-runs.yaml new file mode 100644 index 0000000..f54d044 --- /dev/null +++ b/.github/workflows/_delete-workflow-runs.yaml @@ -0,0 +1,66 @@ +name: 删除旧的工作流运行 +on: + workflow_dispatch: + inputs: + days: + description: '为每个工作流保留的运行天数' + required: true + default: '30' + minimum_runs: + description: '为每个工作流保留的最小运行次数' + required: true + default: '6' + delete_workflow_pattern: + description: '工作流的名称或文件名(如果未设置,则针对所有工作流)' + required: false + delete_workflow_by_state_pattern: + description: '按状态筛选工作流:active, deleted, disabled_fork, disabled_inactivity, disabled_manually' + required: true + default: "ALL" + type: choice + options: + - "ALL" + - active + - deleted + - disabled_inactivity + - disabled_manually + delete_run_by_conclusion_pattern: + description: '根据结论删除运行:action_required, cancelled, failure, skipped, success' + required: true + default: "ALL" + type: choice + options: + - "ALL" + - "Unsuccessful: action_required,cancelled,failure,skipped" + - action_required + - cancelled + - failure + - skipped + - success + dry_run: + description: '记录模拟的更改,不执行任何删除操作' + required: false + +jobs: + del_runs: + runs-on: ubuntu-latest + permissions: + actions: write + contents: read + steps: + - name: 删除工作流运行 + uses: Mattraks/delete-workflow-runs@v2 + with: + token: ${{ github.token }} + repository: ${{ github.repository }} + retain_days: ${{ github.event.inputs.days }} + keep_minimum_runs: ${{ github.event.inputs.minimum_runs }} + delete_workflow_pattern: ${{ github.event.inputs.delete_workflow_pattern }} + delete_workflow_by_state_pattern: ${{ github.event.inputs.delete_workflow_by_state_pattern }} + delete_run_by_conclusion_pattern: >- + ${{ + startsWith(github.event.inputs.delete_run_by_conclusion_pattern, 'Unsuccessful:') + && 'action_required,cancelled,failure,skipped' + || github.event.inputs.delete_run_by_conclusion_pattern + }} + dry_run: ${{ github.event.inputs.dry_run }} \ No newline at end of file diff --git a/.github/workflows/deploy-dist-to-surge-tetst.yaml b/.github/workflows/deploy-dist-to-surge-tetst.yaml new file mode 100644 index 0000000..5551f81 --- /dev/null +++ b/.github/workflows/deploy-dist-to-surge-tetst.yaml @@ -0,0 +1,64 @@ +on: + pull_request: + paths: + - "deploy-dist-to-surge/**" + - ".github/workflows/deploy-dist-to-surge-test.yaml" + push: + paths: + - "deploy-dist-to-surge/**" + - ".github/workflows/deploy-dist-to-surge-test.yaml" +env: + TZ: Asia/Shanghai + +jobs: + job: + runs-on: ubuntu-latest + steps: + - name: Checkout code # Required to use the local version of the action + uses: actions/checkout@main + + - name: 准备部署文件 (Testing working_dir and dist_dir) + run: | + mkdir -p test_project/build_output + html="

Test: ${{ github.event_name }}: ${{ github.sha }} - Custom Dirs

" + echo $html > test_project/build_output/index.html + + - name: Deploy with custom working_dir and dist_dir + uses: ./deploy-dist-to-surge # Use local action + id: surge_deploy_custom + with: + working_dir: ./test_project + dist_dir: build_output + domain_suffix: -custom + + - name: Check Surge URL (Custom Dirs) + run: | + echo "Custom dirs deployment URL: ${{ steps.surge_deploy_custom.outputs.url }}" + # Add a basic check if the URL is not empty + if [ -z "${{ steps.surge_deploy_custom.outputs.url }}" ]; then + echo "Error: Surge URL for custom dirs is empty!" + exit 1 + fi + + - name: 准备部署文件 (Testing default dist_dir) + run: | + mkdir dist + html="

Test: ${{ github.event_name }}: ${{ github.sha }} - Default Dist

" + echo $html > dist/index.html + + - name: Deploy with default dist_dir + uses: ./deploy-dist-to-surge # Use local action + id: surge_deploy_default + with: + domain_suffix: -default + + - name: Check Surge URL (Default Dist) + run: | + echo "Default dist deployment URL: ${{ steps.surge_deploy_default.outputs.url }}" + # Add a basic check if the URL is not empty + if [ -z "${{ steps.surge_deploy_default.outputs.url }}" ]; then + echo "Error: Surge URL for default dist is empty!" + exit 1 + fi + # The following line was from the old version and is redundant as we check specific outputs above. + # echo "steps.surge_deploy.outputs.url: ${{ steps.surge_deploy.outputs.url }}" diff --git a/.github/workflows/docker-build-push-test.yaml b/.github/workflows/docker-build-push-test.yaml new file mode 100644 index 0000000..2c4fdf8 --- /dev/null +++ b/.github/workflows/docker-build-push-test.yaml @@ -0,0 +1,134 @@ +# name: _打包推送镜像 +on: + workflow_dispatch: + pull_request: + paths: + - 'docker-build-push/**' + - '.github/workflows/docker-build-push-test.yaml' + push: + paths: + - 'docker-build-push/**' + - '.github/workflows/docker-build-push-test.yaml' +env: + TZ: Asia/Shanghai + +jobs: + build-and-push-ghcr: + runs-on: ubuntu-latest + env: + # https://github.com/docker/metadata-action/tree/v5/?tab=readme-ov-file#semver + # Event: push, Ref: refs/head/main, Tags: main + # Event: push tag, Ref: refs/tags/v1.2.3, Tags: 1.2.3, 1.2, 1, latest + # Event: push tag, Ref: refs/tags/v2.0.8-rc1, Tags: 2.0.8-rc1 + metadata-action-tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + steps: + - name: 🔑 登录 GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: 🐳 构建并推送 Docker 镜像 + uses: yanhao98/composite-actions/docker-build-push@main + with: + file: ./Dockerfile.test + platforms: linux/amd64,linux/arm64 + push: true + load: false + meta_images: ghcr.io/${{ github.repository }} + meta_tags: ${{ env.metadata-action-tags }} + cache-from: type=gha,scope=${{ github.workflow }} + cache-to: type=gha,scope=${{ github.workflow }} + + build-and-push-multi-registry: + runs-on: ubuntu-latest + steps: + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: yanhao98/composite-actions/docker-build-push@main + id: docker-build-push + with: + file: ./Dockerfile.test + platforms: linux/amd64,linux/arm64 + push: true + load: false + meta_images: | + docker.io/${{ vars.DOCKERHUB_USERNAME }}/docker-example + ghcr.io/${{ github.repository }} + meta_tags: | # https://github.com/docker/metadata-action + type=raw,value=latest,enable=true + + cache-gha: + runs-on: ubuntu-latest + steps: + - uses: yanhao98/composite-actions/docker-build-push@main + id: docker-build-push + with: + file: ./Dockerfile.test + platforms: linux/amd64 + push: false + load: true + build-args: | + SHA=${{ github.sha }} + BUILDKIT_INLINE_CACHE=1 + # ##### + # scope: https://github.com/docker/build-push-action/issues/252#issuecomment-881050512 + # cache-to: mode=max + # ##### + cache-from: type=gha,scope=${{ github.workflow }} + cache-to: type=gha,scope=${{ github.workflow }} + - name: Check Docker image + run: | + set -x; + docker images; + docker run --rm ${{ steps.docker-build-push.outputs.imageid }} cat /root/sha.txt; + + cache-local: + runs-on: ubuntu-latest + steps: + - name: 🗄️ 缓存Docker层 + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-IMAGE_NAME-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-IMAGE_NAME- + - uses: yanhao98/composite-actions/docker-build-push@main + id: docker-build-push + with: + file: ./Dockerfile.test + platforms: linux/amd64 + push: false + load: true + build-args: | + SHA=${{ github.sha }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new + # Temp fix: 如果要在一个 job 中多次使用 buildx 缓存才需要这个步骤。 + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: 🔄 更新缓存 + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + - name: Check Docker image + run: | + set -x; + docker images; + docker run --rm ${{ steps.docker-build-push.outputs.imageid }} cat /root/sha.txt; diff --git a/.github/workflows/npm-build-fix-to-nexus-test.yaml b/.github/workflows/npm-build-fix-to-nexus-test.yaml new file mode 100644 index 0000000..b36ae53 --- /dev/null +++ b/.github/workflows/npm-build-fix-to-nexus-test.yaml @@ -0,0 +1,26 @@ +on: + pull_request: + paths: + - "npm-build-fix-to-nexus/**" + - ".github/workflows/npm-build-fix-to-nexus-test.yaml" + push: + paths: + - "npm-build-fix-to-nexus/**" + - ".github/workflows/npm-build-fix-to-nexus-test.yaml" +env: + TZ: Asia/Shanghai + +jobs: + upload_npm_fix_to_nexus: + runs-on: ubuntu-latest + steps: + - name: Prepare + run: | + mkdir npm-build-fix-to-nexus + - uses: yanhao98/composite-actions/npm-build-fix-to-nexus@main + with: + package_json_url: 'https://www.unpkg.com/fuck-your-code/package.json' + pack_workspace: './npm-build-fix-to-nexus' + build_command: 'whoami' + nexus_post_url: 'https://nexus.oo1.dev/service/rest/v1/components?repository=npm-hosted' + nexus_auth: ${{ secrets.NEXUS_AUTH }} \ No newline at end of file diff --git a/.github/workflows/setup-node-environment-test.yaml b/.github/workflows/setup-node-environment-test.yaml new file mode 100644 index 0000000..7812850 --- /dev/null +++ b/.github/workflows/setup-node-environment-test.yaml @@ -0,0 +1,126 @@ +on: + pull_request: + paths: + - "setup-node-environment/**" + - ".github/workflows/setup-node-environment-test.yaml" + push: + paths: + - "setup-node-environment/**" + - ".github/workflows/setup-node-environment-test.yaml" +env: + TZ: Asia/Shanghai + package_json_content: | + { + "packageManager": "pnpm@10.6.5", + "dependencies": { + "bun": "^1.2.5" + } + } + +concurrency: + group: ${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + generate_lock: + runs-on: ubuntu-latest + outputs: + lock_file_content: ${{ steps.generate_lock.outputs.lock_file_content }} + steps: + - uses: pnpm/action-setup@v4 + with: + version: latest + standalone: true + - id: generate_lock + env: + CI: 'false' + run: | + set -x; + cat < package.json + ${{ env.package_json_content }} + EOF + pnpm config list + cat package.json + pnpm install --lockfile-only + echo "lock_file_content<> $GITHUB_OUTPUT + cat pnpm-lock.yaml >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + tests: + needs: generate_lock + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + npmrc_content: + - '' + - | + use-node-version=22.14.0 # https://pnpm.io/zh/npmrc#use-node-version + lock_file: + - 'true' + - 'false' + cwd: + - '' + - 'test' + steps: + # - uses: actions/checkout@main + - name: 打印 matrix + run: | + echo "🤖---- 打印 matrix ----🤖" + echo "npmrc_content: ${{ matrix.npmrc_content }}" + echo "lock_file: ${{ matrix.lock_file }}" + echo "cwd: ${{ matrix.cwd }}" + echo "GITHUB_WORKSPACE: ${{ github.workspace }}" + + - name: Create test directory + if: matrix.cwd != '' + run: | + mkdir -p ${{ matrix.cwd }} + set -x; + ls -l -R . + pwd + + - name: Create .npmrc + working-directory: ${{ matrix.cwd }} + if: matrix.npmrc_content != '' + run: | + cat < .npmrc + ${{ matrix.npmrc_content }} + EOF + set -x; + ls -l -R . + pwd + cat .npmrc + + - name: Create package.json + working-directory: ${{ matrix.cwd }} + run: | + mkdir -p ${{ github.workspace }}/.git + cat < package.json + ${{ env.package_json_content }} + EOF + set -x; + ls -l -R . + pwd + cat package.json + + - name: Create pnpm-lock.yaml + working-directory: ${{ matrix.cwd }} + if: matrix.lock_file == 'true' + run: | + cat < pnpm-lock.yaml + ${{ needs.generate_lock.outputs.lock_file_content }} + EOF + set -x; + ls -l -R . + pwd + cat pnpm-lock.yaml + + - name: ⚙️ 设置 Node 环境 + uses: yanhao98/composite-actions/setup-node-environment@main + with: + working-directory: ${{ matrix.cwd }} diff --git a/.github/workflows/upload-to-alist-example-test.yaml b/.github/workflows/upload-to-alist-example-test.yaml new file mode 100644 index 0000000..a39de77 --- /dev/null +++ b/.github/workflows/upload-to-alist-example-test.yaml @@ -0,0 +1,50 @@ +on: + pull_request: + paths: + - "upload-to-alist/**" + - ".github/workflows/upload-to-alist-example-test.yaml" + push: + paths: + - "upload-to-alist/**" + - ".github/workflows/upload-to-alist-example-test.yaml" + +env: + TZ: Asia/Shanghai + +jobs: + upload: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@main + + - name: 📝 生成构建产物的文件名 + id: filename + run: | + PROJECT_NAME=$(echo ${{ github.repository }} | cut -d'/' -f2) + DATE=$(date '+%Y%m%d_%H%M') + SHORT_HASH=$(git rev-parse --short HEAD) + + FILENAME="${PROJECT_NAME}_${DATE}_${SHORT_HASH}.txt" + echo "📝 生成的文件名: $FILENAME" + echo "FILENAME=${FILENAME}" >> $GITHUB_OUTPUT + + - name: Create test file + run: | + cat > ${{ steps.filename.outputs.FILENAME }} << EOF + # 测试文件 + + - 项目: ${{ github.repository }} + - 分支: ${{ github.ref_name }} + - 提交: $(git rev-parse HEAD) + - 时间: $(date '+%Y-%m-%d %H:%M:%S %Z') + - 触发事件: ${{ github.event_name }} + EOF + + - uses: yanhao98/composite-actions/upload-to-alist@main + with: + alist_url: ${{ vars.ALIST_URL }} + alist_username: ${{ secrets.ALIST_USERNAME }} + alist_password: ${{ secrets.ALIST_PASSWORD }} + alist_target: ${{ vars.alist_target_base }}/github-actions/upload-to-alist/${{ steps.filename.outputs.FILENAME }} + file: ${{ steps.filename.outputs.FILENAME }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..b875d81 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,7 @@ +{ + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/changelog" + ] +} \ No newline at end of file diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 0000000..dddf08f --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,12 @@ +# syntax=docker/dockerfile:1.14-labs +FROM alpine:latest + +ARG SHA=unspecified +ENV SHA=$SHA + +COPY <.surge.sh`。 | `false` | `''` | +| `surge_token` | **已弃用.** 用于部署的 Surge 令牌。建议在工作流程中将其设置为环境变量 `SURGE_TOKEN`。 | `false` | (从 `SURGE_TOKEN` 环境变量读取,如果未设置则使用硬编码令牌) | + + +#### Outputs + +| Name | Description | +|-------|------------------| +| `url` | The Preview URL. | + +#### Example Usage + +```yaml +name: Deploy to Surge + +on: [push] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@main + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + # Assuming your build process outputs to 'build_output' inside 'frontend' directory + - name: Build application + run: | + cd frontend + npm install + npm run build + - name: Deploy to Surge + uses: YOUR_USERNAME/YOUR_REPONAME/deploy-dist-to-surge@main # Replace with your actual repo path + with: + working_dir: frontend + dist_dir: build_output + env: + SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }} # Recommended way to provide the token +``` + +### Other Actions + +- **`docker-build-push`**: Builds and pushes Docker images. +- **`npm-build-fix-to-nexus`**: Builds and fixes NPM packages for Nexus. +- **`setup-node-environment`**: Sets up a Node.js environment. +- **`upload-to-alist`**: Uploads files to Alist. + +--- +*Original reference note (can be removed or integrated elsewhere):* +- https://github.com/renovatebot/renovate/blob/81fc75630b0b43fb4b89a0b65c1086d487e65d2e/.github/actions/setup-node/action.yml diff --git a/deploy-dist-to-surge/action.yml b/deploy-dist-to-surge/action.yml new file mode 100644 index 0000000..f1f95fd --- /dev/null +++ b/deploy-dist-to-surge/action.yml @@ -0,0 +1,37 @@ +name: "Deploy dist to Surge" +description: "部署 dist 到 Surge" +inputs: + working_dir: + description: '执行 Surge 部署的工作目录。默认为仓库根目录。' + required: false + default: '.' + dist_dir: + description: '包含要部署的构建产物的目录。如果指定了 `working_dir`,则相对于 `working_dir`,否则相对于仓库根目录。' + required: false + default: 'dist' + domain_suffix: + description: '部署时用于创建唯一域名的后缀(例如,用于在同一工作流程中测试多个实例)。最终域名将是 `.surge.sh`。' + required: false + default: '' +outputs: + url: + description: "Preview URL" + value: ${{ steps.surge_deploy.outputs.url }} +runs: + using: "composite" + steps: + - name: 部署到 Surge + shell: bash + id: surge_deploy + # https://github.com/afc163/surge-preview + # https://github.com/Tencent/tdesign-vue-next/pull/1604#issuecomment-1236244550 + # https://github.com/Tencent/tdesign-vue-next/blob/03036a19adccf4657d7792e3a61a6c6a7d902e3e/.github/workflows/preview-publish.yml + # https://github.com/Tencent/tdesign/blob/0c0c9b63897c05d10c58e1a1e36feda2cb99eca7/.github/workflows/preview.yml#L40 + run: | + if [ "${{ inputs.working_dir }}" != "." ]; then cd ${{ inputs.working_dir }}; fi + export DEPLOY_DOMAIN_PREFIX=${{ github.sha }}${{ inputs.domain_suffix }} + export DEPLOY_DOMAIN=https://${DEPLOY_DOMAIN_PREFIX}.surge.sh + cp ${{ inputs.dist_dir }}/index.html ${{ inputs.dist_dir }}/200.html + npx surge --project ./${{ inputs.dist_dir }} --domain $DEPLOY_DOMAIN --token d843de16b331c626f10771245c56ed93 # npx surge token + echo the preview URL is $DEPLOY_DOMAIN + echo "url=$DEPLOY_DOMAIN" >> $GITHUB_OUTPUT diff --git a/docker-build-push/action.yml b/docker-build-push/action.yml new file mode 100644 index 0000000..0dec0e5 --- /dev/null +++ b/docker-build-push/action.yml @@ -0,0 +1,99 @@ +# - https://docs.docker.com/build/ci/github-actions/push-multi-registries/ + +# - https://github.com/docker/build-push-action + +name: "打包推送 Docker 镜像" +description: "打包 Docker 镜像并推送到 Docker Hub" +inputs: + file: + description: "Dockerfile 文件路径" + default: "./Dockerfile" + required: false + context: + description: "Docker 构建上下文路径" + default: "." + required: false + platforms: + description: "Docker 构建平台" + default: "linux/amd64,linux/arm64" + required: false + push: + description: "是否推送 Docker 镜像" + default: "false" + required: false + load: + description: "是否加载 Docker 镜像" + default: "false" + required: false + meta_images: + description: "docker/metadata-action 的 images 参数" + required: false + meta_tags: + description: "docker/metadata-action 的 tags 参数" + required: false + cache-from: + description: "docker/build-push-action 的 cache-from 参数" + required: false + cache-to: + description: "docker/build-push-action 的 cache-to 参数" + required: false + build-args: + description: "Docker 构建参数,格式如: KEY1=VALUE1,KEY2=VALUE2" + required: false +outputs: + imageid: + description: "Docker 镜像 ID" + value: ${{ steps.build.outputs.imageid }} +runs: + using: "composite" + steps: + - name: echo Start + shell: bash + run: echo -e "\n[$(date +'%Y-%m-%d %H:%M:%S')] 🤖🤖🤖 Start 🤖🤖🤖" + + - id: check-git-folder + shell: bash + run: | + # 🤖 判断是否存在 .git 文件夹 🤖 + [ -d .git ] && { echo "🤖 Found .git folder"; echo "git-folder-exists=true" >> $GITHUB_OUTPUT; } || { echo "🤖 No .git folder found"; echo "git-folder-exists=false" >> $GITHUB_OUTPUT; } + - uses: actions/checkout@main + if: steps.check-git-folder.outputs.git-folder-exists == 'false' + with: + filter: blob:none + show-progress: false + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + context: git + # flavor + images: ${{ inputs.meta_images }} + tags: ${{ inputs.meta_tags }} + # sep-tags: ',' + # sep-labels: ',' + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + id: build + uses: docker/build-push-action@v6 + with: + file: ${{ inputs.file }} + context: ${{ inputs.context }} + platforms: ${{ inputs.platforms }} + push: ${{ inputs.push }} + load: ${{ inputs.load }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: ${{ inputs.cache-from }} + cache-to: ${{ inputs.cache-to }} + build-args: ${{ inputs.build-args }} + + - name: echo End + shell: bash + run: echo -e "\n[$(date +'%Y-%m-%d %H:%M:%S')] 🤖🤖🤖 End 🤖🤖🤖" \ No newline at end of file diff --git a/npm-build-fix-to-nexus/action.yml b/npm-build-fix-to-nexus/action.yml new file mode 100644 index 0000000..c17b7a7 --- /dev/null +++ b/npm-build-fix-to-nexus/action.yml @@ -0,0 +1,71 @@ +name: '发布修复包到 Nexus' +description: '发布 npm 修复包到 Nexus' +inputs: + package_json_url: + description: 'package.json 文件的 URL' + required: true + pack_workspace: + description: '打包工作目录' + required: true + build_command: + description: '构建命令' + required: true + nexus_post_url: + description: 'Nexus URL' + required: true + nexus_auth: + description: 'Nexus 认证信息' + required: true +runs: + using: 'composite' + steps: + - name: 下载 package.json 文件 + shell: bash + run: wget ${{ inputs.package_json_url }} -O /tmp/package.json + - name: 更新版本号 + shell: bash + run: | + # 获取当前版本 + VERSION=$(node -p "require('/tmp/package.json').version") + + # 分解版本号 + IFS='.' read -r major minor patch <<< "$VERSION" + + # 增加patch版本 + new_patch=$((patch + 1)) + + # 生成新版本号基础部分 + NEW_VERSION="$major.$minor.$new_patch" + + # 添加-fix后缀 + NEW_VERSION="$NEW_VERSION-fix" + + # 添加时间戳 + TIMESTAMP=$(date -u +"%Y%m%d%H%M") + NEW_VERSION="$NEW_VERSION.$TIMESTAMP" + + echo "Current version: $VERSION" + echo "New version: $NEW_VERSION" + + # 更新package.json中的版本号 + sed -i "s/\"version\": \".*\"/\"version\": \"$NEW_VERSION\"/" /tmp/package.json + - uses: yanhao98/composite-actions/setup-node-environment@main + - name: 构建项目 + shell: bash + run: ${{ inputs.build_command }} + - name: 打包 tgz + id: pack_tgz + working-directory: ${{ inputs.pack_workspace }} + shell: bash + run: | + rm -rf *.tgz + cp /tmp/package.json . + tgz=$(npm pack) + echo "tgz=$tgz" >> $GITHUB_OUTPUT + - name: 上传到 Nexus + working-directory: ${{ inputs.pack_workspace }} + shell: bash + run: | + curl -i -X POST "${{ inputs.nexus_post_url }}" \ + -H "Authorization: Basic ${{ inputs.nexus_auth }}" \ + -F "npm.asset=@${{ steps.pack_tgz.outputs.tgz }}" diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..d3e2a5c --- /dev/null +++ b/renovate.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "https://git.1-h.cc/examples/renovate-example/raw/branch/main/default.json5", + ":automergeAll" + ], + "dependencyDashboard": false, + "packageRules": [ + { "minimumReleaseAge": "1 days", "matchPackageNames": [ "*" ] } + ], + "ignoreDeps": [], + "ignorePaths": [ + "package.json", + ".npmrc", + "Dockerfile.test" + ] +} \ No newline at end of file diff --git a/setup-node-environment/action.yml b/setup-node-environment/action.yml new file mode 100644 index 0000000..0922bee --- /dev/null +++ b/setup-node-environment/action.yml @@ -0,0 +1,230 @@ +# 🔗 链接: +# 源文件: https://github.com/obytes/react-native-template-obytes/blob/master/.github/actions/setup-node-pnpm-install/action.yml +# 复合操作文档: https://docs.github.com/en/actions/creating-actions/creating-a-composite-action + +# ✍️ 描述: +# 这是一个复合操作,意味着它可以在其他操作中使用。 +# 它几乎用于所有工作流中,以设置环境并安装依赖项。 +# 在此处更新包管理器或 Node 版本将反映在所有工作流中。 + +# 👀 使用示例: +# - name : 📦 设置 Node + PNPM + 安装依赖 +# uses: ./.github/actions/setup-node-pnpm-install + +name: '设置 Node 环境' +description: '设置 pnpm + Node.js + 安装依赖项' +inputs: + pnpm_standalone: # 是否将 pnpm 用作独立包 https://github.com/pnpm/action-setup?tab=readme-ov-file#standalone + description: '是否将 pnpm 用作独立包' + required: false + default: 'false' + # https://github.com/pnpm/action-setup/blob/d648c2dd069001a242c621c8306af467f150e99d/action.yml#L18C3-L18C20 + working-directory: + description: '工作目录' + required: false + default: '.' +runs: + using: 'composite' + steps: + - id: check-git-folder + shell: bash + run: | + # 🤖----判断是否存在 .git 文件夹----🤖 # + [ -d ${{ github.workspace }}/.git ] && { echo "🤖 找到 .git 文件夹"; echo "git-folder-exists=true" >> $GITHUB_OUTPUT; } || { echo "🤖 未找到 .git 文件夹"; echo "git-folder-exists=false" >> $GITHUB_OUTPUT; } + + - uses: actions/checkout@main + if: steps.check-git-folder.outputs.git-folder-exists == 'false' + with: + # fetch-depth: 0 # 0 代表完整检出,semantic-release 需要 + filter: blob:none # 我们不需要所有 blob,只需要完整的树 + show-progress: false + + - id: prepare + shell: bash + working-directory: ${{ inputs.working-directory }} + run: | + # 🤖---- 准备环境变量 ----🤖 # + + # --- 1. 包管理器 --- + echo "::group::📦 获取包管理器信息" + pkg_manager=$(node -p "try { require('./package.json').packageManager } catch(e) { '' }") + echo "从 package.json 获取的包管理器: $pkg_manager" + echo "::endgroup::" + + # --- 2. 确定 PNPM 版本 --- + echo "::group::🔧 确定 PNPM 版本" + pnpm_version="" + # 如果 packageManager 为空、undefined 或 null,则使用最新的 pnpm + if [ -z "$pkg_manager" ] || [ "$pkg_manager" == "undefined" ] || [ "$pkg_manager" == "null" ]; then + pnpm_version="latest" + echo "未指定或无效的 packageManager,使用 pnpm 版本: latest" + else + # 如果指定了 pnpm 版本(例如 "pnpm@8.6.0"),则提取版本号 + if [[ "$pkg_manager" == pnpm* ]]; then + pnpm_version=$(echo "$pkg_manager" | cut -d '@' -f 2) + echo "使用 package.json 中的 pnpm 版本: $pnpm_version" + else + echo "指定了非 pnpm 包管理器: $pkg_manager。将由 pnpm/action-setup 处理版本。" + # 让 pnpm/action-setup 根据 corepack 或其默认设置决定版本 + pnpm_version="" # 如果不是 pnpm 或未正确指定,则显式设置为空 + fi + fi + echo "::endgroup::" + + # --- 3. 检查 PNPM 安装情况 --- + echo "::group::🔍 检查 PNPM 安装情况" + is_pnpm_installed="false" + pnpm_executable_path="" + installed_pnpm_version="无法获取" # 版本检查失败时的默认值 + + # 使用 command -v 检查 pnpm 是否在 PATH 中并且可执行 + if command -v pnpm >/dev/null 2>&1; then + pnpm_executable_path=$(command -v pnpm) + echo "找到 PNPM 可执行文件于: $pnpm_executable_path" + # 尝试获取版本号,但不因失败而退出 + if pnpm_version_output=$(pnpm --version 2>/dev/null); then + installed_pnpm_version="$pnpm_version_output" + is_pnpm_installed="true" # 确认 pnpm 可执行且能获取版本 + echo "已安装的 PNPM 版本: $installed_pnpm_version" + else + # pnpm 命令存在但获取版本失败,可能安装不完整或有问题 + is_pnpm_installed="true" # 标记为已安装,因为命令存在 + echo "警告:PNPM 命令存在,但无法获取版本号。可能安装不完整或存在问题。" + echo "将继续执行,后续步骤可能会重新安装或修复。" + fi + else + echo "PNPM 未在 PATH 中找到或不可执行。" + is_pnpm_installed="false" + fi + echo "::endgroup::" + + # --- 4. 检查 PNPM Lock 文件 --- + echo "::group::📄 检查 PNPM Lock 文件" + has_pnpm_lock="false" + if [ -f pnpm-lock.yaml ]; then + has_pnpm_lock="true" + echo "找到 pnpm-lock.yaml。" + else + echo "未找到 pnpm-lock.yaml。" + fi + echo "::endgroup::" + + # --- 5. 确定 Node.js 版本并清理 .npmrc --- + echo "::group::⚙️ 确定 Node.js 版本并清理 .npmrc" + node_version="lts/*" # 默认 Node 版本 + if [ ! -f .npmrc ]; then + echo "未找到 .npmrc,创建一个空文件。" + touch .npmrc + else + echo "找到 .npmrc 文件。" + fi + + # 从 .npmrc 读取 use-node-version + node_version_in_npmrc=$(sed -n 's/.*use-node-version=\([0-9.]*\).*/\1/p' .npmrc) + if [ -n "$node_version_in_npmrc" ]; then + node_major_version_in_npmrc=$(echo "$node_version_in_npmrc" | cut -d. -f1) + if [ -n "$node_major_version_in_npmrc" ]; then + node_version="$node_major_version_in_npmrc" + echo ".npmrc 中指定的 Node 版本: $node_version_in_npmrc -> 使用主版本: $node_version" + else + echo "无法从 .npmrc ($node_version_in_npmrc) 提取主 Node 版本。使用默认值: $node_version" + fi + else + echo ".npmrc 中未找到 'use-node-version'。使用默认值: $node_version" + fi + + # 清理 .npmrc:删除 use-node-version 和 node-mirror 行 + echo "正在清理 .npmrc..." + # 使用 -i.bak 以兼容不同 sed 版本,并在创建备份后删除 + sed -i.bak -e '/use-node-version/d' -e '/node-mirror/d' .npmrc + [ -f .npmrc.bak ] && rm .npmrc.bak + echo ".npmrc 已清理。" + echo "::endgroup::" + + # --- 6. 设置输出 --- + echo "::group::🚀 设置 GitHub Actions 输出" + echo "packageManager=${pkg_manager}" >> $GITHUB_OUTPUT + echo "pnpmVersion=${pnpm_version}" >> $GITHUB_OUTPUT + echo "pnpmInstalled=${is_pnpm_installed}" >> $GITHUB_OUTPUT + echo "pnpmLockExists=${has_pnpm_lock}" >> $GITHUB_OUTPUT + echo "nodeVersion=${node_version}" >> $GITHUB_OUTPUT + echo "输出已设置:" + printf " %-16s %s\n" "packageManager:" "${pkg_manager}" + printf " %-16s %s\n" "pnpmVersion:" "${pnpm_version}" + printf " %-16s %s\n" "pnpmInstalled:" "${is_pnpm_installed}" + printf " %-16s %s\n" "pnpmLockExists:" "${has_pnpm_lock}" + printf " %-16s %s\n" "nodeVersion:" "${node_version}" + echo "::endgroup::" + + - uses: pnpm/action-setup@v4 # https://github.com/pnpm/action-setup?tab=readme-ov-file#inputs + if: steps.prepare.outputs.pnpmInstalled == 'false' + with: + # https://github.com/pnpm/action-setup/blob/master/action.yml + version: ${{ steps.prepare.outputs.pnpmVersion }} + standalone: ${{ inputs.pnpm_standalone }} + package_json_file: ${{ inputs.working-directory }}/package.json + + - uses: actions/setup-node@v4 # https://github.com/actions/setup-node?tab=readme-ov-file#usage + with: + node-version: ${{ steps.prepare.outputs.nodeVersion }} + cache: '' + + - id: pnpm-store-dir + shell: bash + run: | + echo "🤖---- 获取 PNPM 存储目录 ----🤖" + echo "PNPM 存储目录: $(pnpm store path)" + echo "pnpmStoreDir=$(pnpm store path)" >> $GITHUB_OUTPUT + + - id: cache-pnpm-restore + uses: actions/cache/restore@v4 # https://github.com/actions/cache/blob/main/restore/action.yml + with: + path: ${{ steps.pnpm-store-dir.outputs.pnpmStoreDir }} + key: ${{ runner.os }}-pnpm-store-id${{ github.event.repository.id }}-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store-id${{ github.event.repository.id }}- + ${{ runner.os }}-pnpm-store- + + # https://github.com/pnpm/pnpm/issues/7192#issuecomment-2353298966 + - run: echo "package-import-method=hardlink" >> .npmrc + shell: bash + + - if: steps.cache-pnpm-restore.outputs.cache-hit == 'true' + working-directory: ${{ inputs.working-directory }} + shell: bash + run: | + echo "🤖---- 缓存命中,安装依赖 ----🤖" + pnpm install --prefer-offline + # --frozen-lockfile + # ERR_PNPM_NO_LOCKFILE  Cannot install with "frozen-lockfile" because pnpm-lock.yaml is absent + + - if: steps.cache-pnpm-restore.outputs.cache-hit != 'true' + working-directory: ${{ inputs.working-directory }} + shell: bash + run: | + echo "🤖---- 缓存未命中,安装依赖 ----🤖" + set -x; + pnpm fetch # https://pnpm.io/zh/cli/fetch + pnpm install --prefer-offline + # --frozen-lockfile + # ERR_PNPM_NO_LOCKFILE  Cannot install with "frozen-lockfile" because pnpm-lock.yaml is absent + pnpm store prune + + - name: 📊 显示缓存命中状态 + run: | + echo "cache-hit: ${{ steps.cache-pnpm-restore.outputs.cache-hit }}" + shell: bash + + - id: cache-pnpm-save + if: always() && steps.cache-pnpm-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ steps.pnpm-store-dir.outputs.pnpmStoreDir }} + key: ${{ steps.cache-pnpm-restore.outputs.cache-primary-key }} +# rm -r node_modules +# pnpm fetch +# pnpm install --offline +# pnpm store prune +# # +# rm -r node_modules +# pnpm install --offline diff --git a/upload-to-alist/action.yml b/upload-to-alist/action.yml new file mode 100644 index 0000000..0b00695 --- /dev/null +++ b/upload-to-alist/action.yml @@ -0,0 +1,87 @@ +name: '上传文件到 Alist' +description: '将文件上传到 Alist 服务器' +inputs: + alist_url: + description: 'Alist 服务器地址(不包含末尾的 /)' + required: true + default: 'https://alist.oo1.dev' + alist_username: + description: 'Alist 用户名' + required: true + alist_password: + description: 'Alist 密码' + required: true + alist_target: + description: 'Alist 目标路径(完整路径,如:/folder/subfolder/file.zip)' + required: true + file: + description: '要上传的文件路径(完整路径,如:./path/to/file.zip)' + required: true + +runs: + using: 'composite' + steps: + - name: 检查文件 + shell: bash + run: | + if [ ! -f "${{ inputs.file }}" ]; then + echo "错误: 文件 '${{ inputs.file }}' 不存在" + exit 1 + fi + + # 获取文件大小 + file_size=$(stat -f%z "${{ inputs.file }}" 2>/dev/null || stat -c%s "${{ inputs.file }}") + echo "文件大小: $file_size 字节" + + if [ "$file_size" -eq 0 ]; then + echo "错误: 文件为空" + exit 1 + fi + + - name: 获取 Alist Token + shell: bash + id: get_token + run: | + response=$(curl -s --location '${{ inputs.alist_url }}/api/auth/login' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode "Username=${{ inputs.alist_username }}" \ + --data-urlencode "Password=${{ inputs.alist_password }}") + + if ! echo "$response" | jq -e . >/dev/null 2>&1; then + echo "错误: 服务器返回的不是有效的 JSON 响应" + echo "响应内容: $response" + exit 1 + fi + + token=$(echo "$response" | jq -r ".data.token") + + if [ "$token" = "null" ] || [ -z "$token" ]; then + echo "错误: 获取 token 失败" + echo "响应内容: $response" + exit 1 + fi + + echo "成功获取 token" + echo "token=$token" >> $GITHUB_OUTPUT + + - name: 上传文件到 Alist + shell: bash + run: | + response=$(curl -s --location --request PUT '${{ inputs.alist_url }}/api/fs/form' \ + --header "Authorization: ${{ steps.get_token.outputs.token }}" \ + --header "file-path: ${{ inputs.alist_target }}" \ + --form "file=@${{ inputs.file }}") + + if ! echo "$response" | jq -e . >/dev/null 2>&1; then + echo "错误: 服务器返回的不是有效的 JSON 响应" + echo "响应内容: $response" + exit 1 + fi + + if echo "$response" | jq -e '.code == 200' >/dev/null 2>&1; then + echo "✅ 文件上传成功" + else + echo "❌ 文件上传失败" + echo "错误响应: $response" + exit 1 + fi \ No newline at end of file