From 8a78f96564e8b82ef89320f78928d4783847bbca Mon Sep 17 00:00:00 2001 From: Lukin Date: Thu, 12 Feb 2026 23:31:02 +0800 Subject: [PATCH 1/7] ci: configure workflow for GHCR-only releases - Remove Docker Hub dependency, use only GitHub Container Registry - Update image naming logic for personal fork - Simplify release notes to show GHCR images only --- .github/workflows/auto-release-pipeline.yml | 946 ++++++++++---------- 1 file changed, 469 insertions(+), 477 deletions(-) diff --git a/.github/workflows/auto-release-pipeline.yml b/.github/workflows/auto-release-pipeline.yml index 4e29adc7..19607c38 100644 --- a/.github/workflows/auto-release-pipeline.yml +++ b/.github/workflows/auto-release-pipeline.yml @@ -4,7 +4,7 @@ on: push: branches: - main - workflow_dispatch: # 支持手动触发 + workflow_dispatch: # 支持手动触发 permissions: contents: write @@ -16,494 +16,486 @@ jobs: # 跳过由GitHub Actions创建的提交,避免死循环 if: github.event.pusher.name != 'github-actions[bot]' && !contains(github.event.head_commit.message, '[skip ci]') steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} + - 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: | - # 检查提交消息是否包含强制发布标记([force release]) - COMMIT_MSG=$(git log -1 --pretty=%B | tr -d '\r') - echo "Latest commit message:" - echo "$COMMIT_MSG" + - name: Check if version bump is needed + id: check + run: | + # 检查提交消息是否包含强制发布标记([force release]) + COMMIT_MSG=$(git log -1 --pretty=%B | tr -d '\r') + echo "Latest commit message:" + echo "$COMMIT_MSG" - FORCE_RELEASE=false - if echo "$COMMIT_MSG" | grep -qi "\[force release\]"; then - echo "Detected [force release] marker, forcing version bump" - FORCE_RELEASE=true - fi - - # 检测是否是合并提交 - PARENT_COUNT=$(git rev-list --parents -n 1 HEAD | wc -w) - PARENT_COUNT=$((PARENT_COUNT - 1)) - echo "Parent count: $PARENT_COUNT" - - if [ "$PARENT_COUNT" -gt 1 ]; then - # 合并提交:获取合并进来的所有文件变更 - echo "Detected merge commit, getting all merged changes" - # 获取合并基准点 - MERGE_BASE=$(git merge-base HEAD^1 HEAD^2 2>/dev/null || echo "") - if [ -n "$MERGE_BASE" ]; then - # 获取从合并基准到 HEAD 的所有变更 - CHANGED_FILES=$(git diff --name-only $MERGE_BASE..HEAD) - else - # 如果无法获取合并基准,使用第二个父提交 - CHANGED_FILES=$(git diff --name-only HEAD^2..HEAD) + FORCE_RELEASE=false + if echo "$COMMIT_MSG" | grep -qi "\[force release\]"; then + echo "Detected [force release] marker, forcing version bump" + FORCE_RELEASE=true fi - else - # 普通提交:获取相对于上一个提交的变更 - CHANGED_FILES=$(git diff --name-only HEAD~1..HEAD 2>/dev/null || git diff --name-only $(git rev-list --max-parents=0 HEAD)..HEAD) - fi - - 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 [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "Manual workflow trigger detected, forcing version bump" - echo "needs_bump=true" >> $GITHUB_OUTPUT - elif [ "$FORCE_RELEASE" = true ]; then - echo "Force release marker detected, forcing version bump" - echo "needs_bump=true" >> $GITHUB_OUTPUT - elif [ "$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 + # 检测是否是合并提交 + PARENT_COUNT=$(git rev-list --parents -n 1 HEAD | wc -w) + PARENT_COUNT=$((PARENT_COUNT - 1)) + echo "Parent count: $PARENT_COUNT" - - 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} - - # 获取VERSION文件中的版本 - FILE_VERSION=$(cat VERSION | tr -d '[:space:]') - echo "VERSION file: $FILE_VERSION" - - # 比较tag版本和文件版本,取较大值 - function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; } - - if version_gt "$FILE_VERSION" "$TAG_VERSION"; then - VERSION="$FILE_VERSION" - echo "Using VERSION file: $VERSION (newer than tag)" - else - VERSION="$TAG_VERSION" - echo "Using tag version: $VERSION (newer or equal to file)" - fi - - 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 - echo "new_tag=v$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文件 - 添加 [skip ci] 以避免再次触发 - git add VERSION - git commit -m "chore: sync VERSION file with release ${{ steps.next_version.outputs.new_tag }} [skip ci]" - - # 构建前端并推送到 web-dist 分支 - - name: Setup Node.js - if: steps.check.outputs.needs_bump == 'true' - uses: actions/setup-node@v4 - with: - node-version: '18' - cache: 'npm' - cache-dependency-path: web/admin-spa/package-lock.json - - - name: Build Frontend - if: steps.check.outputs.needs_bump == 'true' - run: | - echo "Building frontend for version ${{ steps.next_version.outputs.new_version }}..." - cd web/admin-spa - npm ci - npm run build - echo "Frontend build completed" - - - name: Push Frontend Build to web-dist Branch - if: steps.check.outputs.needs_bump == 'true' - run: | - # 创建临时目录 - TEMP_DIR=$(mktemp -d) - echo "Using temp directory: $TEMP_DIR" - - # 复制构建产物到临时目录 - cp -r web/admin-spa/dist/* "$TEMP_DIR/" - - # 检查 web-dist 分支是否存在 - if git ls-remote --heads origin web-dist | grep -q web-dist; then - echo "Checking out existing web-dist branch" - git fetch origin web-dist:web-dist - git checkout web-dist - else - echo "Creating new web-dist branch" - git checkout --orphan web-dist - fi - - # 清空当前目录(保留 .git) - git rm -rf . 2>/dev/null || true - - # 复制构建产物 - cp -r "$TEMP_DIR"/* . - - # 添加 README - cat > README.md << EOF - # Claude Relay Service - Web Frontend Build - - This branch contains the pre-built frontend assets for Claude Relay Service. - - **DO NOT EDIT FILES IN THIS BRANCH DIRECTLY** - - These files are automatically generated by the CI/CD pipeline. - - Version: ${{ steps.next_version.outputs.new_version }} - Build Date: $(date -u +"%Y-%m-%d %H:%M:%S UTC") - EOF - - # 创建 .gitignore 文件以排除 node_modules - cat > .gitignore << EOF - node_modules/ - *.log - .DS_Store - .env - EOF - - # 只添加必要的文件,排除 node_modules - git add --all -- ':!node_modules' - git commit -m "chore: update frontend build for v${{ steps.next_version.outputs.new_version }} [skip ci]" - git push origin web-dist --force - - # 切换回主分支 - git checkout main - - # 清理临时目录 - rm -rf "$TEMP_DIR" - - echo "Frontend build pushed to web-dist branch successfully" - - - name: Install git-cliff - if: steps.check.outputs.needs_bump == '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.check.outputs.needs_bump == 'true' - id: changelog - run: | - # 获取上一个tag以来的更新日志 - LATEST_TAG=$(git describe --tags --abbrev=0 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.check.outputs.needs_bump == 'true' - run: | - NEW_TAG="${{ steps.next_version.outputs.new_tag }}" - git tag -a "$NEW_TAG" -m "Release $NEW_TAG" - git push origin HEAD:main "$NEW_TAG" - - - name: Prepare image names - id: image_names - if: steps.check.outputs.needs_bump == 'true' - run: | - DOCKER_USERNAME="${{ secrets.DOCKERHUB_USERNAME }}" - if [ -z "$DOCKER_USERNAME" ]; then - DOCKER_USERNAME="weishaw" - fi - - DOCKER_IMAGE=$(echo "${DOCKER_USERNAME}/claude-relay-service" | tr '[:upper:]' '[:lower:]') - GHCR_IMAGE=$(echo "ghcr.io/${{ github.repository_owner }}/claude-relay-service" | tr '[:upper:]' '[:lower:]') - - { - echo "docker_image=${DOCKER_IMAGE}" - echo "ghcr_image=${GHCR_IMAGE}" - } >> "$GITHUB_OUTPUT" - - - name: Create GitHub Release - if: steps.check.outputs.needs_bump == '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 ${{ steps.image_names.outputs.docker_image }}:${{ steps.next_version.outputs.new_tag }} - docker pull ${{ steps.image_names.outputs.docker_image }}:latest - docker pull ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }} - docker pull ${{ steps.image_names.outputs.ghcr_image }}:latest - ``` - - ## 📦 主要更新 - - ${{ steps.changelog.outputs.content }} - - ## 📋 完整更新日志 - - 查看 [所有版本](https://github.com/${{ github.repository }}/releases) - draft: false - prerelease: false - generate_release_notes: true - - # 自动清理旧的tags和releases(保持最近50个) - - name: Cleanup old tags and releases - if: steps.check.outputs.needs_bump == 'true' - continue-on-error: true - env: - TAGS_TO_KEEP: 50 - run: | - echo "🧹 自动清理旧版本,保持最近 $TAGS_TO_KEEP 个tag..." - - # 获取所有版本tag并按版本号排序(从旧到新) - echo "正在获取所有tags..." - ALL_TAGS=$(git ls-remote --tags origin | grep -E 'refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' | awk '{print $2}' | sed 's|refs/tags/||' | sort -V) - - # 检查是否获取到tags - if [ -z "$ALL_TAGS" ]; then - echo "⚠️ 未找到任何版本tag" - exit 0 - fi - - TOTAL_COUNT=$(echo "$ALL_TAGS" | wc -l) - - echo "📊 当前tag统计:" - echo "- 总数: $TOTAL_COUNT" - echo "- 配置保留: $TAGS_TO_KEEP" - - if [ "$TOTAL_COUNT" -gt "$TAGS_TO_KEEP" ]; then - DELETE_COUNT=$((TOTAL_COUNT - TAGS_TO_KEEP)) - echo "- 将要删除: $DELETE_COUNT 个最旧的tag" - - # 获取要删除的tags(最老的) - TAGS_TO_DELETE=$(echo "$ALL_TAGS" | head -n "$DELETE_COUNT") - - # 显示将要删除的版本范围 - OLDEST_TO_DELETE=$(echo "$TAGS_TO_DELETE" | head -1) - NEWEST_TO_DELETE=$(echo "$TAGS_TO_DELETE" | tail -1) - echo "" - echo "🗑️ 将要删除的版本范围:" - echo "- 从: $OLDEST_TO_DELETE" - echo "- 到: $NEWEST_TO_DELETE" - - echo "" - echo "开始执行删除..." - SUCCESS_COUNT=0 - FAIL_COUNT=0 - - for tag in $TAGS_TO_DELETE; do - echo -n " 删除 $tag ... " - - # 先检查release是否存在 - if gh release view "$tag" >/dev/null 2>&1; then - # Release存在,删除release会同时删除tag - if gh release delete "$tag" --yes --cleanup-tag 2>/dev/null; then - echo "✅ (release+tag)" - SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) - else - echo "❌ (release删除失败)" - FAIL_COUNT=$((FAIL_COUNT + 1)) - fi + if [ "$PARENT_COUNT" -gt 1 ]; then + # 合并提交:获取合并进来的所有文件变更 + echo "Detected merge commit, getting all merged changes" + # 获取合并基准点 + MERGE_BASE=$(git merge-base HEAD^1 HEAD^2 2>/dev/null || echo "") + if [ -n "$MERGE_BASE" ]; then + # 获取从合并基准到 HEAD 的所有变更 + CHANGED_FILES=$(git diff --name-only $MERGE_BASE..HEAD) else - # Release不存在,只删除tag - if git push origin --delete "$tag" 2>/dev/null; then - echo "✅ (仅tag)" - SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) - else - echo "⏭️ (已不存在)" - FAIL_COUNT=$((FAIL_COUNT + 1)) - fi + # 如果无法获取合并基准,使用第二个父提交 + CHANGED_FILES=$(git diff --name-only HEAD^2..HEAD) fi - done - - echo "" - echo "📊 清理结果:" - echo "- 成功删除: $SUCCESS_COUNT" - echo "- 失败/跳过: $FAIL_COUNT" - - # 重新获取并显示保留的版本范围 - echo "" - echo "正在验证清理结果..." - REMAINING_TAGS=$(git ls-remote --tags origin | grep -E 'refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' | awk '{print $2}' | sed 's|refs/tags/||' | sort -V) - REMAINING_COUNT=$(echo "$REMAINING_TAGS" | wc -l) - OLDEST=$(echo "$REMAINING_TAGS" | head -1) - NEWEST=$(echo "$REMAINING_TAGS" | tail -1) - - echo "✅ 清理完成!" - echo "" - echo "📌 当前保留的版本:" - echo "- 最旧版本: $OLDEST" - echo "- 最新版本: $NEWEST" - echo "- 版本总数: $REMAINING_COUNT" - - # 验证是否达到预期 - if [ "$REMAINING_COUNT" -le "$TAGS_TO_KEEP" ]; then - echo "- 状态: ✅ 符合预期(≤$TAGS_TO_KEEP)" else - echo "- 状态: ⚠️ 超出预期(某些tag可能删除失败)" + # 普通提交:获取相对于上一个提交的变更 + CHANGED_FILES=$(git diff --name-only HEAD~1..HEAD 2>/dev/null || git diff --name-only $(git rev-list --max-parents=0 HEAD)..HEAD) fi - else - echo "✅ 当前tag数量($TOTAL_COUNT)未超过限制($TAGS_TO_KEEP),无需清理" - fi - # Docker构建步骤 - - name: Set up QEMU - if: steps.check.outputs.needs_bump == 'true' - uses: docker/setup-qemu-action@v3 + echo "Changed files:" + echo "$CHANGED_FILES" - - name: Set up Docker Buildx - if: steps.check.outputs.needs_bump == 'true' - uses: docker/setup-buildx-action@v3 + # 检查是否只有无关文件(.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" - - name: Log in to Docker Hub - if: steps.check.outputs.needs_bump == 'true' - uses: docker/login-action@v3 - with: - registry: docker.io - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + # 检查是否是手动触发 + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "Manual workflow trigger detected, forcing version bump" + echo "needs_bump=true" >> $GITHUB_OUTPUT + elif [ "$FORCE_RELEASE" = true ]; then + echo "Force release marker detected, forcing version bump" + echo "needs_bump=true" >> $GITHUB_OUTPUT + elif [ "$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: Log in to GitHub Container Registry - if: steps.check.outputs.needs_bump == 'true' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} + - 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} - - name: Build and push Docker image - if: steps.check.outputs.needs_bump == 'true' - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: | - ${{ steps.image_names.outputs.docker_image }}:${{ steps.next_version.outputs.new_tag }} - ${{ steps.image_names.outputs.docker_image }}:latest - ${{ steps.image_names.outputs.docker_image }}:${{ steps.next_version.outputs.new_version }} - ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }} - ${{ steps.image_names.outputs.ghcr_image }}:latest - ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_version }} - labels: | - org.opencontainers.image.version=${{ steps.next_version.outputs.new_version }} - org.opencontainers.image.revision=${{ github.sha }} - org.opencontainers.image.source=https://github.com/${{ github.repository }} - cache-from: type=gha - cache-to: type=gha,mode=max + # 获取VERSION文件中的版本 + FILE_VERSION=$(cat VERSION | tr -d '[:space:]') + echo "VERSION file: $FILE_VERSION" - - name: Send Telegram Notification - if: steps.check.outputs.needs_bump == 'true' && env.TELEGRAM_BOT_TOKEN != '' && env.TELEGRAM_CHAT_ID != '' - env: - TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} - TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} - DOCKER_IMAGE: ${{ steps.image_names.outputs.docker_image }} - GHCR_IMAGE: ${{ steps.image_names.outputs.ghcr_image }} - 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 ${DOCKER_IMAGE}:${TAG}"$'\n' - MESSAGE+="docker pull ${DOCKER_IMAGE}:latest"$'\n' - MESSAGE+="docker pull ${GHCR_IMAGE}:${TAG}"$'\n' - MESSAGE+="docker pull ${GHCR_IMAGE}: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/${DOCKER_IMAGE%/*}/claude-relay-service)"$'\n' - MESSAGE+="• [GHCR](https://ghcr.io/${GHCR_IMAGE#ghcr.io/})"$'\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 @- + # 比较tag版本和文件版本,取较大值 + function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; } + + if version_gt "$FILE_VERSION" "$TAG_VERSION"; then + VERSION="$FILE_VERSION" + echo "Using VERSION file: $VERSION (newer than tag)" + else + VERSION="$TAG_VERSION" + echo "Using tag version: $VERSION (newer or equal to file)" + fi + + 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 + echo "new_tag=v$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文件 - 添加 [skip ci] 以避免再次触发 + git add VERSION + git commit -m "chore: sync VERSION file with release ${{ steps.next_version.outputs.new_tag }} [skip ci]" + + # 构建前端并推送到 web-dist 分支 + - name: Setup Node.js + if: steps.check.outputs.needs_bump == 'true' + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: web/admin-spa/package-lock.json + + - name: Build Frontend + if: steps.check.outputs.needs_bump == 'true' + run: | + echo "Building frontend for version ${{ steps.next_version.outputs.new_version }}..." + cd web/admin-spa + npm ci + npm run build + echo "Frontend build completed" + + - name: Push Frontend Build to web-dist Branch + if: steps.check.outputs.needs_bump == 'true' + run: | + # 创建临时目录 + TEMP_DIR=$(mktemp -d) + echo "Using temp directory: $TEMP_DIR" + + # 复制构建产物到临时目录 + cp -r web/admin-spa/dist/* "$TEMP_DIR/" + + # 检查 web-dist 分支是否存在 + if git ls-remote --heads origin web-dist | grep -q web-dist; then + echo "Checking out existing web-dist branch" + git fetch origin web-dist:web-dist + git checkout web-dist + else + echo "Creating new web-dist branch" + git checkout --orphan web-dist + fi + + # 清空当前目录(保留 .git) + git rm -rf . 2>/dev/null || true + + # 复制构建产物 + cp -r "$TEMP_DIR"/* . + + # 添加 README + cat > README.md << EOF + # Claude Relay Service - Web Frontend Build + + This branch contains the pre-built frontend assets for Claude Relay Service. + + **DO NOT EDIT FILES IN THIS BRANCH DIRECTLY** + + These files are automatically generated by the CI/CD pipeline. + + Version: ${{ steps.next_version.outputs.new_version }} + Build Date: $(date -u +"%Y-%m-%d %H:%M:%S UTC") + EOF + + # 创建 .gitignore 文件以排除 node_modules + cat > .gitignore << EOF + node_modules/ + *.log + .DS_Store + .env + EOF + + # 只添加必要的文件,排除 node_modules + git add --all -- ':!node_modules' + git commit -m "chore: update frontend build for v${{ steps.next_version.outputs.new_version }} [skip ci]" + git push origin web-dist --force + + # 切换回主分支 + git checkout main + + # 清理临时目录 + rm -rf "$TEMP_DIR" + + echo "Frontend build pushed to web-dist branch successfully" + + - name: Install git-cliff + if: steps.check.outputs.needs_bump == '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.check.outputs.needs_bump == 'true' + id: changelog + run: | + # 获取上一个tag以来的更新日志 + LATEST_TAG=$(git describe --tags --abbrev=0 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.check.outputs.needs_bump == 'true' + run: | + NEW_TAG="${{ steps.next_version.outputs.new_tag }}" + git tag -a "$NEW_TAG" -m "Release $NEW_TAG" + git push origin HEAD:main "$NEW_TAG" + + - name: Prepare image names + id: image_names + if: steps.check.outputs.needs_bump == 'true' + run: | + # 只使用 GitHub Container Registry + GHCR_IMAGE=$(echo "ghcr.io/${{ github.repository_owner }}/claude-relay-service" | tr '[:upper:]' '[:lower:]') + + { + echo "ghcr_image=${GHCR_IMAGE}" + } >> "$GITHUB_OUTPUT" + + - name: Create GitHub Release + if: steps.check.outputs.needs_bump == '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 镜像 (GHCR) + + ```bash + docker pull ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }} + docker pull ${{ steps.image_names.outputs.ghcr_image }}:latest + ``` + + ## 📦 主要更新 + + ${{ steps.changelog.outputs.content }} + + ## 📋 完整更新日志 + + 查看 [所有版本](https://github.com/${{ github.repository }}/releases) + draft: false + prerelease: false + generate_release_notes: true + + # 自动清理旧的tags和releases(保持最近50个) + - name: Cleanup old tags and releases + if: steps.check.outputs.needs_bump == 'true' + continue-on-error: true + env: + TAGS_TO_KEEP: 50 + run: | + echo "🧹 自动清理旧版本,保持最近 $TAGS_TO_KEEP 个tag..." + + # 获取所有版本tag并按版本号排序(从旧到新) + echo "正在获取所有tags..." + ALL_TAGS=$(git ls-remote --tags origin | grep -E 'refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' | awk '{print $2}' | sed 's|refs/tags/||' | sort -V) + + # 检查是否获取到tags + if [ -z "$ALL_TAGS" ]; then + echo "⚠️ 未找到任何版本tag" + exit 0 + fi + + TOTAL_COUNT=$(echo "$ALL_TAGS" | wc -l) + + echo "📊 当前tag统计:" + echo "- 总数: $TOTAL_COUNT" + echo "- 配置保留: $TAGS_TO_KEEP" + + if [ "$TOTAL_COUNT" -gt "$TAGS_TO_KEEP" ]; then + DELETE_COUNT=$((TOTAL_COUNT - TAGS_TO_KEEP)) + echo "- 将要删除: $DELETE_COUNT 个最旧的tag" + + # 获取要删除的tags(最老的) + TAGS_TO_DELETE=$(echo "$ALL_TAGS" | head -n "$DELETE_COUNT") + + # 显示将要删除的版本范围 + OLDEST_TO_DELETE=$(echo "$TAGS_TO_DELETE" | head -1) + NEWEST_TO_DELETE=$(echo "$TAGS_TO_DELETE" | tail -1) + echo "" + echo "🗑️ 将要删除的版本范围:" + echo "- 从: $OLDEST_TO_DELETE" + echo "- 到: $NEWEST_TO_DELETE" + + echo "" + echo "开始执行删除..." + SUCCESS_COUNT=0 + FAIL_COUNT=0 + + for tag in $TAGS_TO_DELETE; do + echo -n " 删除 $tag ... " + + # 先检查release是否存在 + if gh release view "$tag" >/dev/null 2>&1; then + # Release存在,删除release会同时删除tag + if gh release delete "$tag" --yes --cleanup-tag 2>/dev/null; then + echo "✅ (release+tag)" + SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) + else + echo "❌ (release删除失败)" + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi + else + # Release不存在,只删除tag + if git push origin --delete "$tag" 2>/dev/null; then + echo "✅ (仅tag)" + SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) + else + echo "⏭️ (已不存在)" + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi + fi + done + + echo "" + echo "📊 清理结果:" + echo "- 成功删除: $SUCCESS_COUNT" + echo "- 失败/跳过: $FAIL_COUNT" + + # 重新获取并显示保留的版本范围 + echo "" + echo "正在验证清理结果..." + REMAINING_TAGS=$(git ls-remote --tags origin | grep -E 'refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' | awk '{print $2}' | sed 's|refs/tags/||' | sort -V) + REMAINING_COUNT=$(echo "$REMAINING_TAGS" | wc -l) + OLDEST=$(echo "$REMAINING_TAGS" | head -1) + NEWEST=$(echo "$REMAINING_TAGS" | tail -1) + + echo "✅ 清理完成!" + echo "" + echo "📌 当前保留的版本:" + echo "- 最旧版本: $OLDEST" + echo "- 最新版本: $NEWEST" + echo "- 版本总数: $REMAINING_COUNT" + + # 验证是否达到预期 + if [ "$REMAINING_COUNT" -le "$TAGS_TO_KEEP" ]; then + echo "- 状态: ✅ 符合预期(≤$TAGS_TO_KEEP)" + else + echo "- 状态: ⚠️ 超出预期(某些tag可能删除失败)" + fi + else + echo "✅ 当前tag数量($TOTAL_COUNT)未超过限制($TAGS_TO_KEEP),无需清理" + fi + + # Docker构建步骤 + - name: Set up QEMU + if: steps.check.outputs.needs_bump == 'true' + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + if: steps.check.outputs.needs_bump == 'true' + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + if: steps.check.outputs.needs_bump == 'true' + uses: docker/login-action@v3 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Log in to GitHub Container Registry + if: steps.check.outputs.needs_bump == 'true' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + if: steps.check.outputs.needs_bump == 'true' + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ steps.image_names.outputs.docker_image }}:${{ steps.next_version.outputs.new_tag }} + ${{ steps.image_names.outputs.docker_image }}:latest + ${{ steps.image_names.outputs.docker_image }}:${{ steps.next_version.outputs.new_version }} + ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }} + ${{ steps.image_names.outputs.ghcr_image }}:latest + ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_version }} + labels: | + org.opencontainers.image.version=${{ steps.next_version.outputs.new_version }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.source=https://github.com/${{ github.repository }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Send Telegram Notification + if: steps.check.outputs.needs_bump == 'true' && env.TELEGRAM_BOT_TOKEN != '' && env.TELEGRAM_CHAT_ID != '' + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} + DOCKER_IMAGE: ${{ steps.image_names.outputs.docker_image }} + GHCR_IMAGE: ${{ steps.image_names.outputs.ghcr_image }} + 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 ${DOCKER_IMAGE}:${TAG}"$'\n' + MESSAGE+="docker pull ${DOCKER_IMAGE}:latest"$'\n' + MESSAGE+="docker pull ${GHCR_IMAGE}:${TAG}"$'\n' + MESSAGE+="docker pull ${GHCR_IMAGE}: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/${DOCKER_IMAGE%/*}/claude-relay-service)"$'\n' + MESSAGE+="• [GHCR](https://ghcr.io/${GHCR_IMAGE#ghcr.io/})"$'\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 @- From fe52a62cdaf5178a9aa5769c4edf98a190cdc30e Mon Sep 17 00:00:00 2001 From: Lukin Date: Thu, 12 Feb 2026 23:32:26 +0800 Subject: [PATCH 2/7] fix(console): transform system role messages for Console API compatibility Claude Console API (e.g., GLM accounts) does not accept messages with role='system' in the messages array, returning 422 error: "Input should be 'user' or 'assistant'" This fix automatically transforms system messages by merging them into the first user message's content, maintaining compatibility with clients like opencode that send system role messages. Changes: - Add _transformSystemMessages() method to merge system content into user messages - Apply transformation in both relayRequest() and relayStreamRequestWithUsageCapture() - Only affects claude-console account type, no impact on official API Fixes issues where opencode users get 422 errors when using Console API accounts. --- .../relay/claudeConsoleRelayService.js | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/src/services/relay/claudeConsoleRelayService.js b/src/services/relay/claudeConsoleRelayService.js index d7dcabc1..1cd55329 100644 --- a/src/services/relay/claudeConsoleRelayService.js +++ b/src/services/relay/claudeConsoleRelayService.js @@ -19,6 +19,68 @@ class ClaudeConsoleRelayService { this.defaultUserAgent = 'claude-cli/2.0.52 (external, cli)' } + /** + * 🔄 转换 messages 数组中的 system role 消息 + * Console API 不支持 role="system",需要将 system 内容合并到第一条 user 消息 + * @param {Object} requestBody - 原始请求体 + * @returns {Object} 转换后的请求体 + */ + _transformSystemMessages(requestBody) { + if (!requestBody || !Array.isArray(requestBody.messages)) { + return requestBody + } + + // 收集所有 system messages 的内容 + const systemContents = [] + const nonSystemMessages = [] + + for (const msg of requestBody.messages) { + if (msg && msg.role === 'system') { + systemContents.push(msg.content) + } else { + nonSystemMessages.push(msg) + } + } + + // 如果没有 system messages,直接返回原请求 + if (systemContents.length === 0) { + return requestBody + } + + // 合并 system 内容到第一条 user 消息 + const systemText = systemContents.join('\n\n') + let transformedMessages = nonSystemMessages + + // 查找第一条 user 消息 + const firstUserIndex = nonSystemMessages.findIndex((m) => m && m.role === 'user') + + if (firstUserIndex !== -1) { + // 将 system 内容前置到第一条 user 消息 + const firstUserMsg = nonSystemMessages[firstUserIndex] + const mergedContent = `${systemText}\n\n${firstUserMsg.content}` + transformedMessages = [...nonSystemMessages] + transformedMessages[firstUserIndex] = { + ...firstUserMsg, + content: mergedContent + } + } else { + // 如果没有 user 消息,创建一个包含 system 内容的 user 消息 + logger.warn( + `⚠️ Console API: No user message found to merge system prompt, creating new user message with system content` + ) + transformedMessages = [{ role: 'user', content: systemText }, ...nonSystemMessages] + } + + logger.debug( + `🔄 Console API: Transformed ${systemContents.length} system message(s) into user message context` + ) + + return { + ...requestBody, + messages: transformedMessages + } + } + // 🚀 转发请求到Claude Console API async relayRequest( requestBody, @@ -163,6 +225,9 @@ class ClaudeConsoleRelayService { model: mappedModel } + // 🔄 转换 system messages (Console API 不支持 role="system") + const transformedRequestBody = this._transformSystemMessages(modifiedRequestBody) + // 模型兼容性检查已经在调度器中完成,这里不需要再检查 // 创建代理agent @@ -219,7 +284,7 @@ class ClaudeConsoleRelayService { const requestConfig = { method: 'POST', url: apiEndpoint, - data: modifiedRequestBody, + data: transformedRequestBody, headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01', @@ -649,6 +714,9 @@ class ClaudeConsoleRelayService { model: mappedModel } + // 🔄 转换 system messages (Console API 不支持 role="system") + const transformedRequestBody = this._transformSystemMessages(modifiedRequestBody) + // 模型兼容性检查已经在调度器中完成,这里不需要再检查 // 创建代理agent @@ -656,7 +724,7 @@ class ClaudeConsoleRelayService { // 发送流式请求 await this._makeClaudeConsoleStreamRequest( - modifiedRequestBody, + transformedRequestBody, account, proxyAgent, clientHeaders, From bdecbdedae862d78bef87eba328bef6181439ff8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 12 Feb 2026 15:37:51 +0000 Subject: [PATCH 3/7] chore: sync VERSION file with release v1.1.276 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 1b2d909c..cbeb664f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.274 +1.1.276 From bf847b8aaf3e92d4ee017a62148c467f4e9376ab Mon Sep 17 00:00:00 2001 From: Lukin Date: Thu, 12 Feb 2026 23:41:14 +0800 Subject: [PATCH 4/7] ci: remove Docker Hub, use only GHCR for releases - Remove Docker Hub login step - Remove docker.io image tags from build - Update Telegram notification to show only GHCR images --- .github/workflows/auto-release-pipeline.yml | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/.github/workflows/auto-release-pipeline.yml b/.github/workflows/auto-release-pipeline.yml index 19607c38..9cdd3f02 100644 --- a/.github/workflows/auto-release-pipeline.yml +++ b/.github/workflows/auto-release-pipeline.yml @@ -410,14 +410,6 @@ jobs: if: steps.check.outputs.needs_bump == 'true' uses: docker/setup-buildx-action@v3 - - name: Log in to Docker Hub - if: steps.check.outputs.needs_bump == 'true' - uses: docker/login-action@v3 - with: - registry: docker.io - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Log in to GitHub Container Registry if: steps.check.outputs.needs_bump == 'true' uses: docker/login-action@v3 @@ -434,9 +426,6 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: | - ${{ steps.image_names.outputs.docker_image }}:${{ steps.next_version.outputs.new_tag }} - ${{ steps.image_names.outputs.docker_image }}:latest - ${{ steps.image_names.outputs.docker_image }}:${{ steps.next_version.outputs.new_version }} ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }} ${{ steps.image_names.outputs.ghcr_image }}:latest ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_version }} @@ -452,7 +441,6 @@ jobs: env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} - DOCKER_IMAGE: ${{ steps.image_names.outputs.docker_image }} GHCR_IMAGE: ${{ steps.image_names.outputs.ghcr_image }} continue-on-error: true run: | @@ -472,17 +460,14 @@ jobs: MESSAGE+="📦 版本号: \`${VERSION}\`"$'\n'$'\n' MESSAGE+="📝 *更新内容:*"$'\n' MESSAGE+="${CHANGELOG_TRUNCATED}"$'\n'$'\n' - MESSAGE+="🐳 *Docker 部署:*"$'\n' + MESSAGE+="🐳 *Docker 部署 (GHCR):*"$'\n' MESSAGE+="\`\`\`bash"$'\n' - MESSAGE+="docker pull ${DOCKER_IMAGE}:${TAG}"$'\n' - MESSAGE+="docker pull ${DOCKER_IMAGE}:latest"$'\n' MESSAGE+="docker pull ${GHCR_IMAGE}:${TAG}"$'\n' MESSAGE+="docker pull ${GHCR_IMAGE}: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/${DOCKER_IMAGE%/*}/claude-relay-service)"$'\n' MESSAGE+="• [GHCR](https://ghcr.io/${GHCR_IMAGE#ghcr.io/})"$'\n'$'\n' MESSAGE+="#ClaudeRelay #Update #v${VERSION//./_}" From 25cbcd313b535b1ff357012b0941634802dfdcb2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 12 Feb 2026 15:42:52 +0000 Subject: [PATCH 5/7] chore: sync VERSION file with release v1.1.277 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index cbeb664f..5de35b89 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.276 +1.1.277 From abc66cc86f92d59edf9bce35ec60d98952acb14c Mon Sep 17 00:00:00 2001 From: Lukin Date: Fri, 13 Feb 2026 11:41:39 +0800 Subject: [PATCH 6/7] fix: patch orphaned tool_use blocks missing tool_result in Claude relay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clients (e.g. opencode) may truncate conversation history in long sessions, leaving tool_use blocks without corresponding tool_result. The upstream Claude API strictly validates this pairing and returns 400. This adds _patchOrphanedToolUse() to claudeRelayService._processRequestBody(), which detects orphaned tool_use IDs and synthesizes error tool_result blocks — the same approach already used in anthropicGeminiBridgeService for the Antigravity path. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- src/services/relay/claudeRelayService.js | 85 ++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/services/relay/claudeRelayService.js b/src/services/relay/claudeRelayService.js index 71e8aaa5..24f2bd90 100644 --- a/src/services/relay/claudeRelayService.js +++ b/src/services/relay/claudeRelayService.js @@ -962,6 +962,89 @@ class ClaudeRelayService { } } + // 🔧 修补孤立的 tool_use(缺少对应 tool_result) + // 客户端在长对话中可能截断历史消息,导致 tool_use 丢失对应的 tool_result, + // 上游 Claude API 严格校验每个 tool_use 必须紧跟 tool_result,否则返回 400。 + _patchOrphanedToolUse(messages) { + if (!Array.isArray(messages) || messages.length === 0) { + return messages + } + + const SYNTHETIC_TEXT = '[tool_result missing; tool execution interrupted]' + const makeSyntheticResult = (toolUseId) => ({ + type: 'tool_result', + tool_use_id: toolUseId, + is_error: true, + content: [{ type: 'text', text: SYNTHETIC_TEXT }] + }) + + const pendingToolUseIds = [] + const patched = [] + + for (const message of messages) { + if (!message || !Array.isArray(message.content)) { + patched.push(message) + continue + } + + if (message.role === 'assistant') { + if (pendingToolUseIds.length > 0) { + patched.push({ + role: 'user', + content: pendingToolUseIds.map(makeSyntheticResult) + }) + logger.warn( + `🔧 Patched ${pendingToolUseIds.length} orphaned tool_use(s): ${pendingToolUseIds.join(', ')}` + ) + pendingToolUseIds.length = 0 + } + + const toolUseIds = message.content + .filter((part) => part?.type === 'tool_use' && typeof part.id === 'string') + .map((part) => part.id) + if (toolUseIds.length > 0) { + pendingToolUseIds.push(...toolUseIds) + } + + patched.push(message) + continue + } + + if (message.role === 'user' && pendingToolUseIds.length > 0) { + const toolResultIds = new Set( + message.content + .filter((p) => p?.type === 'tool_result' && typeof p.tool_use_id === 'string') + .map((p) => p.tool_use_id) + ) + const missing = pendingToolUseIds.filter((id) => !toolResultIds.has(id)) + + if (missing.length > 0) { + const synthetic = missing.map(makeSyntheticResult) + logger.warn( + `🔧 Patched ${missing.length} missing tool_result(s) in user message: ${missing.join(', ')}` + ) + message.content = [...synthetic, ...message.content] + } + + pendingToolUseIds.length = 0 + } + + patched.push(message) + } + + if (pendingToolUseIds.length > 0) { + patched.push({ + role: 'user', + content: pendingToolUseIds.map(makeSyntheticResult) + }) + logger.warn( + `🔧 Patched ${pendingToolUseIds.length} trailing orphaned tool_use(s): ${pendingToolUseIds.join(', ')}` + ) + } + + return patched + } + // 🔄 处理请求体 _processRequestBody(body, account = null) { if (!body) { @@ -971,6 +1054,8 @@ class ClaudeRelayService { // 使用 safeClone 替代 JSON.parse(JSON.stringify()) 提升性能 const processedBody = safeClone(body) + processedBody.messages = this._patchOrphanedToolUse(processedBody.messages) + // 验证并限制max_tokens参数 this._validateAndLimitMaxTokens(processedBody) From b08c34e40aab341eca7b2d62a519d346eb40f4f9 Mon Sep 17 00:00:00 2001 From: Lukin Date: Fri, 13 Feb 2026 16:26:53 +0800 Subject: [PATCH 7/7] feat: add gpt-5.3-codex-spark model support Add gpt-5.3-codex-spark to model service and config. Also add gpt-5.3-codex to model service (was only in config). Co-Authored-By: Claude Opus 4.6 --- config/models.js | 1 + src/services/modelService.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config/models.js b/config/models.js index 4b2d2da7..aaff30ea 100644 --- a/config/models.js +++ b/config/models.js @@ -32,6 +32,7 @@ const OPENAI_MODELS = [ { value: 'gpt-5.2', label: 'GPT-5.2' }, { value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex' }, { value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex' }, + { value: 'gpt-5.3-codex-spark', label: 'GPT-5.3 Codex Spark' }, { value: 'codex-mini', label: 'Codex Mini' } ] diff --git a/src/services/modelService.js b/src/services/modelService.js index 5a6275a0..3e305272 100644 --- a/src/services/modelService.js +++ b/src/services/modelService.js @@ -52,7 +52,9 @@ class ModelService { 'gpt-5.1-codex', 'gpt-5.1-codex-max', 'gpt-5-2025-08-07', - 'gpt-5-codex' + 'gpt-5-codex', + 'gpt-5.3-codex', + 'gpt-5.3-codex-spark' ] }, gemini: {