diff --git a/.github/RELEASE_PROCESS.md b/.github/RELEASE_PROCESS.md new file mode 100644 index 00000000..9ba1d3c4 --- /dev/null +++ b/.github/RELEASE_PROCESS.md @@ -0,0 +1,94 @@ +# 发布流程说明 + +## 概述 + +本项目采用全自动化的版本管理和发布流程。VERSION文件由GitHub Actions自动维护,无需手动修改。 + +## 自动发布流程 + +### 1. 工作原理 + +1. **代码推送**: 当你推送代码到main分支时 +2. **自动版本更新**: `auto-version-bump.yml`会: + - 检测是否有实质性代码变更(排除.md文件、docs/目录等) + - 如果有代码变更,自动将版本号+1并更新VERSION文件 + - 提交VERSION文件更新到main分支 +3. **自动发布**: `release-on-version.yml`会: + - 检测到只有VERSION文件变更的提交 + - 自动创建Git tag + - 创建GitHub Release + - 构建并推送Docker镜像 + - 发送Telegram通知(如果配置) + +### 2. 工作流文件说明 + +- **auto-version-bump.yml**: 自动检测代码变更并更新VERSION文件 +- **release-on-version.yml**: 检测VERSION文件单独提交并触发发布 +- **docker-publish.yml**: 在tag创建时构建Docker镜像(备用) +- **release.yml**: 在tag创建时生成Release(备用) + +### 3. 版本号规范 + +- 使用语义化版本号:`MAJOR.MINOR.PATCH` +- 默认自动递增PATCH版本(例如:1.1.10 → 1.1.11) +- VERSION文件只包含版本号,不包含`v`前缀 +- Git tag会自动添加`v`前缀 + +### 4. 触发条件 + +**会触发版本更新的文件变更**: +- 源代码文件(.js, .ts, .jsx, .tsx等) +- 配置文件(package.json, Dockerfile等) +- 其他功能性文件 + +**不会触发版本更新的文件变更**: +- Markdown文件(*.md) +- 文档目录(docs/) +- GitHub配置(.github/) +- VERSION文件本身 +- .gitignore、LICENSE等 + +## 使用指南 + +### 正常开发流程 + +1. 进行代码开发和修改 +2. 提交并推送到main分支 +3. 系统自动完成版本更新和发布 + +```bash +# 正常的开发流程 +git add . +git commit -m "feat: 添加新功能" +git push origin main + +# GitHub Actions会自动: +# 1. 检测到代码变更 +# 2. 更新VERSION文件(例如:1.1.10 → 1.1.11) +# 3. 创建新的release和Docker镜像 +``` + +### 跳过版本更新 + +如果只是更新文档或其他非代码文件,系统会自动识别并跳过版本更新。 + +## 故障排除 + +### 版本没有自动更新 + +1. 检查是否有实质性代码变更 +2. 查看GitHub Actions运行日志 +3. 确认推送的是main分支 + +### 需要手动触发发布 + +如果需要手动控制版本: +1. 直接修改VERSION文件 +2. 提交并推送 +3. 系统会检测到VERSION变更并触发发布 + +## 注意事项 + +- **不要**在同一个提交中既修改代码又修改VERSION文件 +- **不要**手动创建tag,让系统自动管理 +- 系统会自动避免死循环(GitHub Actions的提交不会触发新的版本更新) \ No newline at end of file diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml deleted file mode 100644 index d6f33841..00000000 --- a/.github/workflows/auto-release.yml +++ /dev/null @@ -1,267 +0,0 @@ -name: Auto Release on Push to Main - -on: - push: - branches: - - main - paths-ignore: - - '**.md' - - 'docs/**' - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - auto-release: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - - name: Get latest tag - id: get_latest_tag - run: | - # 获取最新的版本标签 - LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") - echo "Latest tag: $LATEST_TAG" - echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT - - # 提取版本号部分(去掉 v 前缀) - VERSION=${LATEST_TAG#v} - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Calculate next version - id: next_version - run: | - VERSION="${{ steps.get_latest_tag.outputs.version }}" - - # 分割版本号为 major.minor.patch - IFS='.' read -r -a version_parts <<< "$VERSION" - MAJOR="${version_parts[0]:-0}" - MINOR="${version_parts[1]:-0}" - PATCH="${version_parts[2]:-0}" - - # 递增 patch 版本号 - NEW_PATCH=$((PATCH + 1)) - NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" - NEW_TAG="v${NEW_VERSION}" - - echo "New version: $NEW_VERSION" - echo "New tag: $NEW_TAG" - echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT - echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT - - - name: Install git-cliff - run: | - wget -q https://github.com/orhun/git-cliff/releases/download/v1.4.0/git-cliff-1.4.0-x86_64-unknown-linux-gnu.tar.gz - tar -xzf git-cliff-1.4.0-x86_64-unknown-linux-gnu.tar.gz - chmod +x git-cliff-1.4.0/git-cliff - sudo mv git-cliff-1.4.0/git-cliff /usr/local/bin/ - - - name: Generate changelog - id: changelog - run: | - # 获取自上次标签以来的提交,如果 git-cliff 输出包含 [unreleased],则移除它 - CHANGELOG=$(git-cliff --config .github/cliff.toml --unreleased --strip header || echo "- 代码优化和改进") - # 移除 [unreleased] 标记 - CHANGELOG=$(echo "$CHANGELOG" | sed 's/\[unreleased\]//g' | sed '/^$/d') - echo "content<> $GITHUB_OUTPUT - echo "$CHANGELOG" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Check if there are changes to release - id: check_changes - run: | - # 检查自上次标签以来是否有新的提交(排除只修改VERSION文件的提交) - LATEST_TAG="${{ steps.get_latest_tag.outputs.latest_tag }}" - if [ "$LATEST_TAG" = "v0.0.0" ]; then - echo "has_changes=true" >> $GITHUB_OUTPUT - else - # 获取除了VERSION文件外的其他文件变更 - CHANGED_FILES=$(git diff --name-only $LATEST_TAG..HEAD | grep -v "^VERSION$" || true) - - if [ -n "$CHANGED_FILES" ]; then - echo "Found changes in files other than VERSION:" - echo "$CHANGED_FILES" - echo "has_changes=true" >> $GITHUB_OUTPUT - else - echo "No significant changes found (only VERSION file changed)" - echo "has_changes=false" >> $GITHUB_OUTPUT - fi - fi - - - name: Create and push tag - if: steps.check_changes.outputs.has_changes == 'true' - run: | - NEW_TAG="${{ steps.next_version.outputs.new_tag }}" - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag -a "$NEW_TAG" -m "Release $NEW_TAG" - git push origin "$NEW_TAG" - - - name: Create Release - if: steps.check_changes.outputs.has_changes == 'true' - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ steps.next_version.outputs.new_tag }} - name: Release ${{ steps.next_version.outputs.new_version }} - body: | - ## 🐳 Docker 镜像 - - ```bash - docker pull ${{ secrets.DOCKERHUB_USERNAME || 'weishaw' }}/claude-relay-service:${{ steps.next_version.outputs.new_tag }} - docker pull ${{ secrets.DOCKERHUB_USERNAME || 'weishaw' }}/claude-relay-service:latest - ``` - - ## 📦 主要更新 - - ${{ steps.changelog.outputs.content }} - - ## 📋 完整更新日志 - - 查看 [所有版本](https://github.com/${{ github.repository }}/releases) - - --- - *此版本由 GitHub Actions 自动发布* - draft: false - prerelease: false - generate_release_notes: true - - - name: Update VERSION file - if: steps.check_changes.outputs.has_changes == 'true' - run: | - # 强制更新 VERSION 文件为最新版本 - echo "${{ steps.next_version.outputs.new_version }}" > VERSION - - # 检查是否有更改 - if git diff --quiet VERSION; then - echo "VERSION file already up to date" - else - git add VERSION - echo "Updated VERSION file to ${{ steps.next_version.outputs.new_version }}" - fi - - - name: Commit VERSION update - if: steps.check_changes.outputs.has_changes == 'true' - run: | - # 提交 VERSION 更新 - if git diff --quiet VERSION; then - echo "No changes to VERSION" - else - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add VERSION - git commit -m "chore: update VERSION to ${{ steps.next_version.outputs.new_version }} for ${{ steps.next_version.outputs.new_tag }} [skip ci]" - git push origin main - fi - - # Docker 构建和推送步骤 - - name: Set up QEMU - if: steps.check_changes.outputs.has_changes == 'true' - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - if: steps.check_changes.outputs.has_changes == 'true' - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - if: steps.check_changes.outputs.has_changes == 'true' - uses: docker/login-action@v3 - with: - registry: docker.io - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push Docker image - if: steps.check_changes.outputs.has_changes == 'true' - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: | - ${{ secrets.DOCKERHUB_USERNAME }}/claude-relay-service:${{ steps.next_version.outputs.new_tag }} - ${{ secrets.DOCKERHUB_USERNAME }}/claude-relay-service:latest - ${{ secrets.DOCKERHUB_USERNAME }}/claude-relay-service:${{ steps.next_version.outputs.new_version }} - labels: | - org.opencontainers.image.version=${{ steps.next_version.outputs.new_version }} - org.opencontainers.image.created=${{ steps.next_version.outputs.created }} - org.opencontainers.image.revision=${{ github.sha }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Send Telegram Notification - if: steps.check_changes.outputs.has_changes == 'true' && env.TELEGRAM_BOT_TOKEN != '' && env.TELEGRAM_CHAT_ID != '' - env: - TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} - TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} - continue-on-error: true - run: | - VERSION="${{ steps.next_version.outputs.new_version }}" - TAG="${{ steps.next_version.outputs.new_tag }}" - REPO="${{ github.repository }}" - - # 获取更新内容并限制长度 - CHANGELOG="${{ steps.changelog.outputs.content }}" - CHANGELOG_TRUNCATED=$(echo "$CHANGELOG" | head -c 1000) - if [ ${#CHANGELOG} -gt 1000 ]; then - CHANGELOG_TRUNCATED="${CHANGELOG_TRUNCATED}..." - fi - - # 构建消息内容 - MESSAGE="🚀 *Claude Relay Service 新版本发布!*"$'\n'$'\n' - MESSAGE+="📦 版本号: \`${VERSION}\`"$'\n'$'\n' - MESSAGE+="📝 *更新内容:*"$'\n' - MESSAGE+="${CHANGELOG_TRUNCATED}"$'\n'$'\n' - MESSAGE+="🐳 *Docker 部署:*"$'\n' - MESSAGE+="\`\`\`bash"$'\n' - MESSAGE+="docker pull weishaw/claude-relay-service:${TAG}"$'\n' - MESSAGE+="docker pull weishaw/claude-relay-service:latest"$'\n' - MESSAGE+="\`\`\`"$'\n'$'\n' - MESSAGE+="🔗 *相关链接:*"$'\n' - MESSAGE+="• [GitHub Release](https://github.com/${REPO}/releases/tag/${TAG})"$'\n' - MESSAGE+="• [完整更新日志](https://github.com/${REPO}/releases)"$'\n' - MESSAGE+="• [Docker Hub](https://hub.docker.com/r/weishaw/claude-relay-service)"$'\n'$'\n' - MESSAGE+="#ClaudeRelay #Update #v${VERSION//./_}" - - # 使用 jq 构建 JSON 并发送 - jq -n \ - --arg chat_id "${TELEGRAM_CHAT_ID}" \ - --arg text "${MESSAGE}" \ - '{ - chat_id: $chat_id, - text: $text, - parse_mode: "Markdown", - disable_web_page_preview: false - }' | \ - curl -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ - -H "Content-Type: application/json" \ - -d @- - - - name: Ensure VERSION file is in sync - if: always() - run: | - # 在工作流结束时确保VERSION文件与最新tag同步 - LATEST_TAG=$(git tag --sort=-version:refname | head -1 || echo "v0.0.0") - if [ "$LATEST_TAG" != "v0.0.0" ]; then - LATEST_VERSION=${LATEST_TAG#v} - CURRENT_VERSION=$(cat VERSION 2>/dev/null || echo "0.0.0") - - if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ]; then - echo "Updating VERSION file to match latest release: $CURRENT_VERSION -> $LATEST_VERSION" - echo "$LATEST_VERSION" > VERSION - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add VERSION - git commit -m "chore: sync VERSION file with release $LATEST_TAG [skip ci]" || echo "No changes to commit" - git push origin main || echo "Nothing to push" - else - echo "VERSION file is already in sync: $CURRENT_VERSION" - fi - fi \ No newline at end of file diff --git a/.github/workflows/auto-version-bump.yml b/.github/workflows/auto-version-bump.yml new file mode 100644 index 00000000..ea5a6834 --- /dev/null +++ b/.github/workflows/auto-version-bump.yml @@ -0,0 +1,102 @@ +name: Auto Version Bump + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + version-bump: + runs-on: ubuntu-latest + # 跳过由GitHub Actions创建的提交,避免死循环 + if: github.event.pusher.name != 'github-actions[bot]' + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Check if version bump is needed + id: check + run: | + # 获取当前提交的文件变更 + CHANGED_FILES=$(git diff --name-only HEAD~1..HEAD 2>/dev/null || git diff --name-only $(git rev-list --max-parents=0 HEAD)..HEAD) + echo "Changed files:" + echo "$CHANGED_FILES" + + # 检查是否只有无关文件(.md, docs/, .github/等) + SIGNIFICANT_CHANGES=false + while IFS= read -r file; do + # 跳过空行 + [ -z "$file" ] && continue + + # 检查是否是需要忽略的文件 + if [[ ! "$file" =~ \.(md|txt)$ ]] && + [[ ! "$file" =~ ^docs/ ]] && + [[ ! "$file" =~ ^\.github/ ]] && + [[ "$file" != "VERSION" ]] && + [[ "$file" != ".gitignore" ]] && + [[ "$file" != "LICENSE" ]]; then + echo "Found significant change in: $file" + SIGNIFICANT_CHANGES=true + break + fi + done <<< "$CHANGED_FILES" + + if [ "$SIGNIFICANT_CHANGES" = true ]; then + echo "Significant changes detected, version bump needed" + echo "needs_bump=true" >> $GITHUB_OUTPUT + else + echo "No significant changes, skipping version bump" + echo "needs_bump=false" >> $GITHUB_OUTPUT + fi + + - name: Get current version + if: steps.check.outputs.needs_bump == 'true' + id: get_version + run: | + # 获取最新的tag版本 + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") + echo "Latest tag: $LATEST_TAG" + + # 从tag中提取版本号 + VERSION=${LATEST_TAG#v} + echo "Current version: $VERSION" + echo "current_version=$VERSION" >> $GITHUB_OUTPUT + + - name: Calculate next version + if: steps.check.outputs.needs_bump == 'true' + id: next_version + run: | + VERSION="${{ steps.get_version.outputs.current_version }}" + + # 分割版本号 + IFS='.' read -r -a version_parts <<< "$VERSION" + MAJOR="${version_parts[0]:-0}" + MINOR="${version_parts[1]:-0}" + PATCH="${version_parts[2]:-0}" + + # 默认递增patch版本 + NEW_PATCH=$((PATCH + 1)) + NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" + + echo "New version: $NEW_VERSION" + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + + - name: Update VERSION file + if: steps.check.outputs.needs_bump == 'true' + run: | + echo "${{ steps.next_version.outputs.new_version }}" > VERSION + + # 配置git + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # 提交VERSION文件 + git add VERSION + git commit -m "chore: bump version to ${{ steps.next_version.outputs.new_version }}" + git push origin main \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 904591d6..4d34f1a4 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -2,11 +2,8 @@ name: Docker Build & Push on: push: - branches: [ main ] tags: - 'v*' - pull_request: - branches: [ main ] workflow_dispatch: env: diff --git a/.github/workflows/release-on-version.yml b/.github/workflows/release-on-version.yml new file mode 100644 index 00000000..2436da3c --- /dev/null +++ b/.github/workflows/release-on-version.yml @@ -0,0 +1,185 @@ +name: Release on Version Change + +on: + push: + branches: + - main + paths: + - 'VERSION' + +permissions: + contents: write + packages: write + +jobs: + check-and-release: + runs-on: ubuntu-latest + # 只处理由GitHub Actions提交的VERSION更新 + if: github.event.pusher.name == 'github-actions[bot]' + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify only VERSION changed + id: verify + run: | + # 获取最后一次提交变更的文件 + CHANGED_FILES=$(git diff --name-only HEAD~1..HEAD) + echo "Changed files: $CHANGED_FILES" + + # 检查是否只有VERSION文件 + if [ "$CHANGED_FILES" = "VERSION" ]; then + echo "Only VERSION file changed, proceeding with release" + echo "should_release=true" >> $GITHUB_OUTPUT + + # 读取新版本号 + NEW_VERSION=$(cat VERSION | tr -d '[:space:]') + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "new_tag=v$NEW_VERSION" >> $GITHUB_OUTPUT + else + echo "Other files changed besides VERSION, skipping release" + echo "should_release=false" >> $GITHUB_OUTPUT + fi + + - name: Install git-cliff + if: steps.verify.outputs.should_release == 'true' + run: | + wget -q https://github.com/orhun/git-cliff/releases/download/v1.4.0/git-cliff-1.4.0-x86_64-unknown-linux-gnu.tar.gz + tar -xzf git-cliff-1.4.0-x86_64-unknown-linux-gnu.tar.gz + chmod +x git-cliff-1.4.0/git-cliff + sudo mv git-cliff-1.4.0/git-cliff /usr/local/bin/ + + - name: Generate changelog + if: steps.verify.outputs.should_release == 'true' + id: changelog + run: | + # 获取上一个tag以来的更新日志 + LATEST_TAG=$(git describe --tags --abbrev=0 --exclude="${{ steps.verify.outputs.new_tag }}" 2>/dev/null || echo "") + if [ -n "$LATEST_TAG" ]; then + # 排除VERSION文件的提交 + CHANGELOG=$(git-cliff --config .github/cliff.toml $LATEST_TAG..HEAD --strip header | grep -v "bump version" | sed '/^$/d' || echo "- 代码优化和改进") + else + CHANGELOG=$(git-cliff --config .github/cliff.toml --strip header || echo "- 初始版本发布") + fi + echo "content<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create and push tag + if: steps.verify.outputs.should_release == 'true' + run: | + NEW_TAG="${{ steps.verify.outputs.new_tag }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "$NEW_TAG" -m "Release $NEW_TAG" + git push origin "$NEW_TAG" + + - name: Create GitHub Release + if: steps.verify.outputs.should_release == 'true' + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ steps.verify.outputs.new_tag }} + name: Release ${{ steps.verify.outputs.new_version }} + body: | + ## 🐳 Docker 镜像 + + ```bash + docker pull ${{ secrets.DOCKERHUB_USERNAME || 'weishaw' }}/claude-relay-service:${{ steps.verify.outputs.new_tag }} + docker pull ${{ secrets.DOCKERHUB_USERNAME || 'weishaw' }}/claude-relay-service:latest + ``` + + ## 📦 主要更新 + + ${{ steps.changelog.outputs.content }} + + ## 📋 完整更新日志 + + 查看 [所有版本](https://github.com/${{ github.repository }}/releases) + draft: false + prerelease: false + generate_release_notes: true + + # Docker构建步骤 + - name: Set up QEMU + if: steps.verify.outputs.should_release == 'true' + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + if: steps.verify.outputs.should_release == 'true' + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + if: steps.verify.outputs.should_release == 'true' + uses: docker/login-action@v3 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + if: steps.verify.outputs.should_release == 'true' + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/claude-relay-service:${{ steps.verify.outputs.new_tag }} + ${{ secrets.DOCKERHUB_USERNAME }}/claude-relay-service:latest + ${{ secrets.DOCKERHUB_USERNAME }}/claude-relay-service:${{ steps.verify.outputs.new_version }} + labels: | + org.opencontainers.image.version=${{ steps.verify.outputs.new_version }} + org.opencontainers.image.revision=${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Send Telegram Notification + if: steps.verify.outputs.should_release == 'true' && env.TELEGRAM_BOT_TOKEN != '' && env.TELEGRAM_CHAT_ID != '' + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} + continue-on-error: true + run: | + VERSION="${{ steps.verify.outputs.new_version }}" + TAG="${{ steps.verify.outputs.new_tag }}" + REPO="${{ github.repository }}" + + # 获取更新内容并限制长度 + CHANGELOG="${{ steps.changelog.outputs.content }}" + CHANGELOG_TRUNCATED=$(echo "$CHANGELOG" | head -c 1000) + if [ ${#CHANGELOG} -gt 1000 ]; then + CHANGELOG_TRUNCATED="${CHANGELOG_TRUNCATED}..." + fi + + # 构建消息内容 + MESSAGE="🚀 *Claude Relay Service 新版本发布!*"$'\n'$'\n' + MESSAGE+="📦 版本号: \`${VERSION}\`"$'\n'$'\n' + MESSAGE+="📝 *更新内容:*"$'\n' + MESSAGE+="${CHANGELOG_TRUNCATED}"$'\n'$'\n' + MESSAGE+="🐳 *Docker 部署:*"$'\n' + MESSAGE+="\`\`\`bash"$'\n' + MESSAGE+="docker pull weishaw/claude-relay-service:${TAG}"$'\n' + MESSAGE+="docker pull weishaw/claude-relay-service:latest"$'\n' + MESSAGE+="\`\`\`"$'\n'$'\n' + MESSAGE+="🔗 *相关链接:*"$'\n' + MESSAGE+="• [GitHub Release](https://github.com/${REPO}/releases/tag/${TAG})"$'\n' + MESSAGE+="• [完整更新日志](https://github.com/${REPO}/releases)"$'\n' + MESSAGE+="• [Docker Hub](https://hub.docker.com/r/weishaw/claude-relay-service)"$'\n'$'\n' + MESSAGE+="#ClaudeRelay #Update #v${VERSION//./_}" + + # 使用 jq 构建 JSON 并发送 + jq -n \ + --arg chat_id "${TELEGRAM_CHAT_ID}" \ + --arg text "${MESSAGE}" \ + '{ + chat_id: $chat_id, + text: $text, + parse_mode: "Markdown", + disable_web_page_preview: false + }' | \ + curl -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ + -H "Content-Type: application/json" \ + -d @- \ No newline at end of file diff --git a/web/admin/app.js b/web/admin/app.js index ed63f0ed..990afadb 100644 --- a/web/admin/app.js +++ b/web/admin/app.js @@ -368,6 +368,40 @@ const app = createApp({ }, methods: { + // 统一的API请求方法,处理token过期等错误 + async apiRequest(url, options = {}) { + try { + const defaultOptions = { + headers: { + 'Authorization': 'Bearer ' + this.authToken, + 'Content-Type': 'application/json', + ...options.headers + }, + ...options + }; + + const response = await fetch(url, defaultOptions); + const data = await response.json(); + + // 检查是否是token过期错误 + if (!response.ok && (response.status === 401 || + (data.error === 'Invalid admin token' || + data.message === 'Invalid or expired admin session'))) { + // 清理本地存储并刷新页面 + localStorage.removeItem('authToken'); + this.authToken = null; + this.isLoggedIn = false; + location.reload(); + return null; + } + + return data; + } catch (error) { + console.error('API request error:', error); + throw error; + } + }, + // 显示确认弹窗 showConfirm(title, message, confirmText = '继续', cancelText = '取消') { return new Promise((resolve) => { @@ -766,18 +800,17 @@ const app = createApp({ ? '/admin/gemini-accounts/generate-auth-url' : '/admin/claude-accounts/generate-auth-url'; - const response = await fetch(endpoint, { + const data = await this.apiRequest(endpoint, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + this.authToken - }, body: JSON.stringify({ proxy: proxy }) }); - const data = await response.json(); + if (!data) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } if (data.success) { if (this.accountForm.platform === 'gemini') { @@ -822,19 +855,18 @@ const app = createApp({ this.createAccountLoading = true; try { // 首先交换authorization code获取token - const exchangeResponse = await fetch('/admin/claude-accounts/exchange-code', { + const exchangeData = await this.apiRequest('/admin/claude-accounts/exchange-code', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + this.authToken - }, body: JSON.stringify({ sessionId: this.oauthData.sessionId, callbackUrl: this.oauthData.callbackUrl }) }); - const exchangeData = await exchangeResponse.json(); + if (!exchangeData) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } if (!exchangeData.success) { // Display detailed error information @@ -857,12 +889,8 @@ const app = createApp({ } // 创建账户 - const createResponse = await fetch('/admin/claude-accounts', { + const createData = await this.apiRequest('/admin/claude-accounts', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + this.authToken - }, body: JSON.stringify({ name: this.accountForm.name, description: this.accountForm.description, @@ -872,7 +900,10 @@ const app = createApp({ }) }); - const createData = await createResponse.json(); + if (!createData) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } if (createData.success) { this.showToast('OAuth账户创建成功!', 'success', '账户创建成功'); @@ -1025,18 +1056,18 @@ const app = createApp({ attempts++; try { - const response = await fetch('/admin/gemini-accounts/poll-auth-status', { + const data = await this.apiRequest('/admin/gemini-accounts/poll-auth-status', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + this.authToken - }, body: JSON.stringify({ sessionId: this.geminiOauthData.sessionId }) }); - const data = await response.json(); + if (!data) { + // 如果token过期,apiRequest会返回null并刷新页面 + this.stopGeminiOAuthPolling(); + return; + } if (data.success) { // 授权成功 @@ -1073,19 +1104,18 @@ const app = createApp({ this.createAccountLoading = true; try { // 首先交换授权码获取 tokens - const tokenResponse = await fetch('/admin/gemini-accounts/exchange-code', { + const tokenData = await this.apiRequest('/admin/gemini-accounts/exchange-code', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + this.authToken - }, body: JSON.stringify({ code: this.geminiOauthData.code, sessionId: this.geminiOauthData.sessionId }) }); - const tokenData = await tokenResponse.json(); + if (!tokenData) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } if (!tokenData.success) { this.showToast(tokenData.message || '授权码交换失败', 'error', '交换失败'); @@ -1105,12 +1135,8 @@ const app = createApp({ } // 创建账户 - const response = await fetch('/admin/gemini-accounts', { + const data = await this.apiRequest('/admin/gemini-accounts', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + this.authToken - }, body: JSON.stringify({ name: this.accountForm.name, description: this.accountForm.description, @@ -1121,7 +1147,10 @@ const app = createApp({ }) }); - const data = await response.json(); + if (!data) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } if (data.success) { this.showToast('Gemini OAuth账户创建成功!', 'success', '账户创建成功'); @@ -1315,7 +1344,8 @@ const app = createApp({ // 记录当前用户名(使用服务器返回的真实用户名) this.currentUser.username = data.username; - this.loadDashboard(); + // 登录成功后刷新页面以重新加载所有数据 + location.reload(); } else { this.loginError = data.message; } @@ -1330,11 +1360,12 @@ const app = createApp({ // 加载当前用户信息 async loadCurrentUser() { try { - const response = await fetch('/web/auth/user', { - headers: { 'Authorization': 'Bearer ' + this.authToken } - }); + const data = await this.apiRequest('/web/auth/user'); - const data = await response.json(); + if (!data) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } if (data.success) { this.currentUser.username = data.user.username; @@ -1380,14 +1411,14 @@ const app = createApp({ try { // 使用后端接口检查更新 - const response = await fetch('/admin/check-updates', { - headers: { - 'Authorization': `Bearer ${this.authToken}` - } - }); + const result = await this.apiRequest('/admin/check-updates'); - if (response.ok) { - const result = await response.json(); + if (!result) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } + + if (result.success) { const data = result.data; this.versionInfo.current = data.current; @@ -1501,12 +1532,8 @@ const app = createApp({ this.changePasswordLoading = true; try { - const response = await fetch('/web/auth/change-password', { + const result = await this.apiRequest('/web/auth/change-password', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + this.authToken - }, body: JSON.stringify({ newUsername: this.changePasswordForm.newUsername || undefined, currentPassword: this.changePasswordForm.currentPassword, @@ -1514,7 +1541,10 @@ const app = createApp({ }) }); - const result = await response.json(); + if (!result) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } if (result.success) { this.showToast('账户信息修改成功,即将退出登录', 'success'); @@ -1563,24 +1593,16 @@ const app = createApp({ async loadDashboard() { try { - const [dashboardResponse, costsResponse] = await Promise.all([ - fetch('/admin/dashboard', { - headers: { 'Authorization': 'Bearer ' + this.authToken } - }), - Promise.all([ - fetch('/admin/usage-costs?period=today', { - headers: { 'Authorization': 'Bearer ' + this.authToken } - }), - fetch('/admin/usage-costs?period=all', { - headers: { 'Authorization': 'Bearer ' + this.authToken } - }) - ]) + const [dashboardData, todayCostsData, totalCostsData] = await Promise.all([ + this.apiRequest('/admin/dashboard'), + this.apiRequest('/admin/usage-costs?period=today'), + this.apiRequest('/admin/usage-costs?period=all') ]); - const dashboardData = await dashboardResponse.json(); - const [todayCostsResponse, totalCostsResponse] = costsResponse; - const todayCostsData = await todayCostsResponse.json(); - const totalCostsData = await totalCostsResponse.json(); + if (!dashboardData || !todayCostsData || !totalCostsData) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } if (dashboardData.success) { const overview = dashboardData.data.overview || {}; @@ -1629,10 +1651,12 @@ const app = createApp({ this.apiKeysLoading = true; console.log('Loading API Keys...'); try { - const response = await fetch('/admin/api-keys', { - headers: { 'Authorization': 'Bearer ' + this.authToken } - }); - const data = await response.json(); + const data = await this.apiRequest('/admin/api-keys'); + + if (!data) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } console.log('API Keys response:', data); @@ -1674,19 +1698,15 @@ const app = createApp({ this.accountsLoading = true; try { // 并行加载 Claude 和 Gemini 账户 - const [claudeResponse, geminiResponse] = await Promise.all([ - fetch('/admin/claude-accounts', { - headers: { 'Authorization': 'Bearer ' + this.authToken } - }), - fetch('/admin/gemini-accounts', { - headers: { 'Authorization': 'Bearer ' + this.authToken } - }) + const [claudeData, geminiData] = await Promise.all([ + this.apiRequest('/admin/claude-accounts'), + this.apiRequest('/admin/gemini-accounts') ]); - const [claudeData, geminiData] = await Promise.all([ - claudeResponse.json(), - geminiResponse.json() - ]); + if (!claudeData || !geminiData) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } // 合并账户数据 const allAccounts = []; @@ -1728,10 +1748,12 @@ const app = createApp({ async loadModelStats() { this.modelStatsLoading = true; try { - const response = await fetch('/admin/model-stats?period=' + this.modelStatsPeriod, { - headers: { 'Authorization': 'Bearer ' + this.authToken } - }); - const data = await response.json(); + const data = await this.apiRequest('/admin/model-stats?period=' + this.modelStatsPeriod); + + if (!data) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } if (data.success) { this.modelStats = data.data || []; @@ -1749,12 +1771,8 @@ const app = createApp({ async createApiKey() { this.createApiKeyLoading = true; try { - const response = await fetch('/admin/api-keys', { + const data = await this.apiRequest('/admin/api-keys', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + this.authToken - }, body: JSON.stringify({ name: this.apiKeyForm.name, tokenLimit: this.apiKeyForm.tokenLimit && this.apiKeyForm.tokenLimit.trim() ? parseInt(this.apiKeyForm.tokenLimit) : null, @@ -1770,7 +1788,10 @@ const app = createApp({ }) }); - const data = await response.json(); + if (!data) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } if (data.success) { // 设置新API Key数据并显示弹窗 @@ -1809,12 +1830,14 @@ const app = createApp({ if (!confirmed) return; try { - const response = await fetch('/admin/api-keys/' + keyId, { - method: 'DELETE', - headers: { 'Authorization': 'Bearer ' + this.authToken } + const data = await this.apiRequest('/admin/api-keys/' + keyId, { + method: 'DELETE' }); - const data = await response.json(); + if (!data) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } if (data.success) { this.showToast('API Key 删除成功', 'success', '删除成功'); @@ -1867,12 +1890,8 @@ const app = createApp({ async updateApiKey() { this.editApiKeyLoading = true; try { - const response = await fetch('/admin/api-keys/' + this.editApiKeyForm.id, { + const data = await this.apiRequest('/admin/api-keys/' + this.editApiKeyForm.id, { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + this.authToken - }, body: JSON.stringify({ tokenLimit: this.editApiKeyForm.tokenLimit && this.editApiKeyForm.tokenLimit.toString().trim() !== '' ? parseInt(this.editApiKeyForm.tokenLimit) : 0, concurrencyLimit: this.editApiKeyForm.concurrencyLimit && this.editApiKeyForm.concurrencyLimit.toString().trim() !== '' ? parseInt(this.editApiKeyForm.concurrencyLimit) : 0, @@ -1886,7 +1905,10 @@ const app = createApp({ }) }); - const data = await response.json(); + if (!data) { + // 如果token过期,apiRequest会返回null并刷新页面 + return; + } if (data.success) { this.showToast('API Key 更新成功', 'success', '更新成功'); @@ -2077,21 +2099,13 @@ const app = createApp({ async loadDashboardModelStats() { console.log('Loading dashboard model stats, period:', this.dashboardModelPeriod, 'authToken:', !!this.authToken); try { - const response = await fetch('/admin/model-stats?period=' + this.dashboardModelPeriod, { - headers: { 'Authorization': 'Bearer ' + this.authToken } - }); + const data = await this.apiRequest('/admin/model-stats?period=' + this.dashboardModelPeriod); - console.log('Model stats response status:', response.status); - - if (!response.ok) { - console.error('Model stats API error:', response.status, response.statusText); - const errorText = await response.text(); - console.error('Error response:', errorText); - this.dashboardModelStats = []; + if (!data) { + // 如果token过期,apiRequest会返回null并刷新页面 return; } - const data = await response.json(); console.log('Model stats response data:', data); if (data.success) {