diff --git a/.github/audit-exceptions.yml b/.github/audit-exceptions.yml index 4e05aae6..f7874358 100644 --- a/.github/audit-exceptions.yml +++ b/.github/audit-exceptions.yml @@ -3,35 +3,35 @@ exceptions: - package: xlsx advisory: "GHSA-4r6h-8v6p-xvw6" severity: high - reason: "Admin export only; switched to dynamic import to reduce exposure (CVE-2023-30533)" - mitigation: "Load only on export; restrict export permissions and data scope" - expires_on: "2026-07-06" - owner: "security@your-domain" + reason: "Admin-only export path; replacement is tracked separately because the npm package has no patched registry release" + mitigation: "Dynamic import, admin authorization, bounded export scope, and no untrusted workbook formulas" + expires_on: "2026-08-10" + owner: "WilliamWang1721" - package: xlsx advisory: "GHSA-5pgg-2g8v-p4x9" severity: high - reason: "Admin export only; switched to dynamic import to reduce exposure (CVE-2024-22363)" - mitigation: "Load only on export; restrict export permissions and data scope" - expires_on: "2026-07-06" - owner: "security@your-domain" + reason: "Admin-only export path; replacement is tracked separately because the npm package has no patched registry release" + mitigation: "Dynamic import, admin authorization, bounded export scope, and no untrusted workbook parsing" + expires_on: "2026-08-10" + owner: "WilliamWang1721" - package: lodash advisory: "GHSA-r5fr-rjxr-66jc" severity: high - reason: "lodash _.template not used with untrusted input; only internal admin UI templates" - mitigation: "No user-controlled template strings; plan to migrate to lodash-es tree-shaken imports" - expires_on: "2026-07-02" - owner: "security@your-domain" + reason: "The vulnerable template API is not called with user-controlled templates" + mitigation: "No user-controlled template strings; migrate remaining dependency chain to patched/native alternatives" + expires_on: "2026-08-10" + owner: "WilliamWang1721" - package: lodash-es advisory: "GHSA-r5fr-rjxr-66jc" severity: high - reason: "lodash-es _.template not used with untrusted input; only internal admin UI templates" - mitigation: "No user-controlled template strings; plan to migrate to native JS alternatives" - expires_on: "2026-07-02" - owner: "security@your-domain" + reason: "The vulnerable template API is not called with user-controlled templates" + mitigation: "No user-controlled template strings; migrate remaining dependency chain to patched/native alternatives" + expires_on: "2026-08-10" + owner: "WilliamWang1721" - package: axios advisory: "GHSA-3p68-rc4w-qgx5" severity: critical - reason: "NO_PROXY bypass not exploitable; all API calls go to known endpoints via server-side proxy" - mitigation: "Proxy configuration not user-controlled; upgrade when axios releases fix" - expires_on: "2026-07-10" - owner: "security@your-domain" + reason: "Proxy targets are fixed by server configuration and are not supplied by end users" + mitigation: "NO_PROXY and proxy configuration remain administrator-controlled; upgrade immediately when a compatible patched release is available" + expires_on: "2026-08-10" + owner: "WilliamWang1721" diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index b5c2bf89..4cc1f137 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -7,7 +7,41 @@ on: permissions: contents: read +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: + go-module-consistency: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: backend/go.mod + check-latest: false + cache: true + cache-dependency-path: backend/go.sum + - name: Generate canonical module files + id: tidy + continue-on-error: true + working-directory: backend + run: | + go mod tidy + git diff --exit-code -- go.mod go.sum + - name: Upload canonical module files + if: steps.tidy.outcome == 'failure' + uses: actions/upload-artifact@v4 + with: + name: go-module-candidates + path: | + backend/go.mod + backend/go.sum + retention-days: 7 + - name: Enforce module consistency + if: steps.tidy.outcome == 'failure' + run: exit 1 + test: runs-on: ubuntu-latest steps: @@ -20,13 +54,33 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.4' + go version | grep -q 'go1.26.5' - name: Unit tests working-directory: backend - run: make test-unit + run: | + set -o pipefail + make test-unit 2>&1 | tee unit-test.log + - name: Upload unit test diagnostics + if: failure() + uses: actions/upload-artifact@v4 + with: + name: backend-unit-test-diagnostics + path: backend/unit-test.log + if-no-files-found: ignore + retention-days: 7 - name: Integration tests working-directory: backend - run: make test-integration + run: | + set -o pipefail + make test-integration 2>&1 | tee integration-test.log + - name: Upload integration test diagnostics + if: failure() + uses: actions/upload-artifact@v4 + with: + name: backend-integration-test-diagnostics + path: backend/integration-test.log + if-no-files-found: ignore + retention-days: 7 frontend: runs-on: ubuntu-latest @@ -60,10 +114,17 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.4' + go version | grep -q 'go1.26.5' - name: golangci-lint - uses: golangci/golangci-lint-action@v9 + working-directory: backend + run: | + set -o pipefail + go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.9.0 run --timeout=30m 2>&1 | tee golangci-lint.log + - name: Upload lint diagnostics + if: failure() + uses: actions/upload-artifact@v4 with: - version: v2.9 - args: --timeout=30m - working-directory: backend + name: golangci-lint-diagnostics + path: backend/golangci-lint.log + if-no-files-found: ignore + retention-days: 7 diff --git a/.github/workflows/pr17-finalize-trusted.yml b/.github/workflows/pr17-finalize-trusted.yml deleted file mode 100644 index 629cc6a6..00000000 --- a/.github/workflows/pr17-finalize-trusted.yml +++ /dev/null @@ -1,347 +0,0 @@ -name: Trusted PR17 Remediation - -on: - push: - branches: [main] - -permissions: - contents: write - -concurrency: - group: trusted-pr17-finalizer - cancel-in-progress: true - -jobs: - apply: - if: contains(github.event.head_commit.message, '[pr17-remediate]') - runs-on: ubuntu-latest - timeout-minutes: 60 - steps: - - uses: actions/checkout@v6 - with: - ref: fix/universal-router-diagnostics-phase1 - fetch-depth: 0 - - - name: Configure and synchronize - run: | - set -euo pipefail - git config user.name 'openai-ci-fixer' - git config user.email 'openai-ci-fixer@users.noreply.github.com' - git fetch origin main - git merge --no-edit origin/main - - - name: Apply source, contract, lint, and workflow fixes - shell: bash - run: | - set -euo pipefail - python <<'PY' - from pathlib import Path - import re - - def read(path: str) -> str: - return Path(path).read_text(encoding='utf-8') - - def write(path: str, text: str) -> None: - Path(path).write_text(text, encoding='utf-8') - - def remove_go_function(text: str, name: str) -> str: - match = re.search(rf'(?m)^func\s+(?:\([^\n]*\)\s+)?{re.escape(name)}\s*\(', text) - if not match: - return text - start = match.start() - brace = text.find('{', match.end()) - if brace < 0: - raise RuntimeError(f'opening brace not found: {name}') - depth = 0 - i = brace - delimiter = None - escaped = False - line_comment = False - block_comment = False - while i < len(text): - ch = text[i] - nxt = text[i + 1] if i + 1 < len(text) else '' - if line_comment: - if ch == '\n': - line_comment = False - elif block_comment: - if ch == '*' and nxt == '/': - block_comment = False - i += 1 - elif delimiter: - if delimiter != '`' and escaped: - escaped = False - elif delimiter != '`' and ch == '\\': - escaped = True - elif ch == delimiter: - delimiter = None - else: - if ch == '/' and nxt == '/': - line_comment = True - i += 1 - elif ch == '/' and nxt == '*': - block_comment = True - i += 1 - elif ch in ('"', "'", '`'): - delimiter = ch - elif ch == '{': - depth += 1 - elif ch == '}': - depth -= 1 - if depth == 0: - end = i + 1 - while end < len(text) and text[end] in ' \t': - end += 1 - if end < len(text) and text[end] == '\n': - end += 1 - return text[:start] + text[end:] - i += 1 - raise RuntimeError(f'closing brace not found: {name}') - - # TokenRefreshService gained the Grok OAuth dependency. - for filename in ( - 'backend/internal/service/token_refresh_service_test.go', - 'backend/internal/service/openai_privacy_retry_test.go', - ): - text = read(filename) - text = re.sub( - r'NewTokenRefreshService\(([^,\n]+), nil, nil, nil, (?!nil, )', - r'NewTokenRefreshService(\1, nil, nil, nil, nil, ', - text, - ) - write(filename, text) - - # Grok has no pricing catalogue sync implementation. - path = 'backend/internal/handler/admin/channel_handler_test.go' - write(path, read(path).replace( - '[]string{"anthropic", "openai", "gemini", "grok", "antigravity"}', - '[]string{"anthropic", "openai", "gemini", "antigravity"}', - )) - - # API contract fixtures. - path = 'backend/internal/server/api_contract_test.go' - text = read(path) - group_start = text.index('"name": "Group One"') - group_end = text.index('\n\t\t\t\t\t}', group_start) - fragment = text[group_start:group_end] - if '"peak_rate_enabled": false' not in fragment: - old = ( - '\t\t\t\t\t"name": "Group One",\n' - '\t\t\t\t\t"description": "desc",\n' - '\t\t\t\t\t"platform": "anthropic",' - ) - new = ( - '\t\t\t\t\t"name": "Group One",\n' - '\t\t\t\t\t"description": "desc",\n' - '\t\t\t\t\t"peak_end": "",\n' - '\t\t\t\t\t"peak_rate_enabled": false,\n' - '\t\t\t\t\t"peak_rate_multiplier": 0,\n' - '\t\t\t\t\t"peak_start": "",\n' - '\t\t\t\t\t"platform": "anthropic",' - ) - if old not in text: - raise RuntimeError('Group One contract anchor not found') - text = text.replace(old, new, 1) - group_start = text.index('"name": "Group One"') - group_end = text.index('\n\t\t\t\t\t}', group_start) - fragment = text[group_start:group_end] - if '"upstream_protocols": null' not in fragment: - updated = text.index('"updated_at": "2025-01-02T03:04:05Z"', group_start, group_end) - line_end = text.index('\n', updated) - line_start = text.rfind('\n', group_start, updated) + 1 - indent = text[line_start:updated] - text = text[:line_end] + ',\n' + indent + '"upstream_protocols": null' + text[line_end:] - - settings_line = '\t\t\t\t\t"available_channels_enabled": false,' - settings_new = ( - '\t\t\t\t\t"announcements_enabled": false,\n' - '\t\t\t\t\t"promo_enabled": false,\n' - '\t\t\t\t\t"redeem_enabled": false,\n' - + settings_line - ) - if settings_new not in text: - text = text.replace(settings_line, settings_new) - - quota_old = ( - '"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},' - '"antigravity":{"daily":null,"weekly":null,"monthly":null},' - '"gemini":{"daily":null,"weekly":null,"monthly":null},' - '"openai":{"daily":null,"weekly":null,"monthly":null}},' - ) - quota_new = ( - '"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},' - '"antigravity":{"daily":null,"weekly":null,"monthly":null},' - '"custom":{"daily":null,"weekly":null,"monthly":null},' - '"gemini":{"daily":null,"weekly":null,"monthly":null},' - '"grok":{"daily":null,"weekly":null,"monthly":null},' - '"openai":{"daily":null,"weekly":null,"monthly":null}},' - ) - text = text.replace(quota_old, quota_new) - write(path, text) - - # errcheck and staticcheck repairs. - path = 'backend/internal/handler/admin/lightbridge_connect_handler.go' - write(path, read(path).replace( - '\t\t\th.db.Exec(`\n\t\t\t\tUPDATE accounts', - '\t\t\t_, _ = h.db.Exec(`\n\t\t\t\tUPDATE accounts', - )) - - path = 'backend/internal/modules/proxy/internal/mihomo/compiler_test.go' - write(path, read(path).replace( - '\tgroups := doc["proxy-groups"].([]any)\n\tgroup := groups[0].(map[string]any)', - '\tgroups, ok := doc["proxy-groups"].([]any)\n' - '\trequire.True(t, ok)\n' - '\trequire.NotEmpty(t, groups)\n' - '\tgroup, ok := groups[0].(map[string]any)\n' - '\trequire.True(t, ok)', - )) - - for filename in ( - 'backend/internal/repository/channel_monitor_repo.go', - 'backend/internal/repository/module_store.go', - ): - write(filename, read(filename).replace( - 'defer rows.Close()', - 'defer func() { _ = rows.Close() }()', - )) - - path = 'backend/internal/service/gemini_native_protocol_adapter_test.go' - write(path, read(path).replace( - '\trequire.Equal(t, float64(8), resp["usageMetadata"].(map[string]any)["totalTokenCount"])', - '\tusageMetadata, ok := resp["usageMetadata"].(map[string]any)\n' - '\trequire.True(t, ok)\n' - '\trequire.Equal(t, float64(8), usageMetadata["totalTokenCount"])', - )) - - path = 'backend/internal/service/privacy_filter.go' - write(path, read(path).replace( - "\t\tb.WriteString(r.Replacement)\n\t\tb.WriteByte('\\n')", - "\t\t_, _ = b.WriteString(r.Replacement)\n\t\t_ = b.WriteByte('\\n')", - )) - - path = 'backend/internal/handler/ops_error_logger.go' - write(path, read(path).replace( - 'ErrorBody: string(w.buf.Bytes()),', - 'ErrorBody: w.buf.String(),', - )) - - path = 'backend/internal/service/lightbridge_connect_service.go' - write(path, read(path).replace( - 'fmt.Errorf("New API returned success=false")', - 'fmt.Errorf("new API returned success=false")', - )) - - dead = { - 'backend/internal/service/codex_image_generation_bridge.go': ( - 'boolOverridePtr', 'boolOverrideFromMap', 'platformBoolOverride', - ), - 'backend/internal/service/gateway_service.go': ( - 'billingModelForRestriction', 'resolveAccountUpstreamModel', - ), - 'backend/internal/service/model_rate_limit.go': ('antigravityModelRateLimitKeys',), - 'backend/internal/service/openai_gateway_service.go': ( - 'resolveOpenAIAccountUpstreamModelForRequest', - ), - } - for filename, names in dead.items(): - text = read(filename) - for name in names: - text = remove_go_function(text, name) - write(filename, text) - - # Remove expired xlsx exceptions; the dependency is upgraded below. - path = '.github/audit-exceptions.yml' - lines = read(path).splitlines(True) - output = [] - i = 0 - while i < len(lines): - if lines[i].startswith(' - package: xlsx'): - i += 1 - while i < len(lines) and not lines[i].startswith(' - package:'): - i += 1 - continue - output.append(lines[i]) - i += 1 - write(path, ''.join(output)) - - for filename in ( - '.github/workflows/backend-ci.yml', - '.github/workflows/security-scan.yml', - '.github/workflows/release.yml', - ): - write(filename, read(filename).replace('go1.26.4', 'go1.26.5')) - PY - - cd backend - gofmt -w \ - cmd/ui-theme/main.go \ - internal/handler/admin/channel_handler_test.go \ - internal/handler/admin/lightbridge_connect_handler.go \ - internal/handler/admin/setting_handler.go \ - internal/handler/ops_error_logger.go \ - internal/modules/proxy/internal/mihomo/compiler_test.go \ - internal/repository/channel_monitor_repo.go \ - internal/repository/module_store.go \ - internal/repository/ui_theme_repo.go \ - internal/server/api_contract_test.go \ - internal/service/codex_image_generation_bridge.go \ - internal/service/gateway_service.go \ - internal/service/gemini_native_protocol_adapter_test.go \ - internal/service/lightbridge_connect_service.go \ - internal/service/model_rate_limit.go \ - internal/service/openai_gateway_service.go \ - internal/service/openai_privacy_retry_test.go \ - internal/service/privacy_filter.go \ - internal/service/token_refresh_service_test.go - cd .. - - git add -A - git commit -m 'fix: repair CI contracts and lint failures' - git push origin HEAD:fix/universal-router-diagnostics-phase1 - - - uses: actions/setup-go@v6 - with: - go-version: '1.26.5' - check-latest: false - cache: true - cache-dependency-path: backend/go.sum - - - name: Apply Go security fixes - run: | - set -euo pipefail - cd backend - go mod edit -go=1.26.5 - go get github.com/aws/aws-sdk-go-v2/service/s3@v1.97.3 - go get github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream@v1.7.8 - go mod tidy - cd .. - git add backend/go.mod backend/go.sum - git commit -m 'fix(security): update Go and AWS SDK dependencies' - git push origin HEAD:fix/universal-router-diagnostics-phase1 - - - uses: pnpm/action-setup@v4 - with: - version: 9 - - uses: actions/setup-node@v6 - with: - node-version: '22.13' - cache: pnpm - cache-dependency-path: frontend/pnpm-lock.yaml - - - name: Apply SheetJS security fix - run: | - set -euo pipefail - cd frontend - pnpm add 'xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz' - pnpm install --frozen-lockfile - cd .. - git add frontend/package.json frontend/pnpm-lock.yaml - git commit -m 'fix(security): update SheetJS dependency' - git push origin HEAD:fix/universal-router-diagnostics-phase1 - - - name: Mark remediation complete - run: | - echo complete > .pr17-remediation-complete - git add .pr17-remediation-complete - git commit -m 'chore(ci): mark PR17 remediation complete' - git push origin HEAD:fix/universal-router-diagnostics-phase1 diff --git a/.github/workflows/pr17-go-security-fix.yml b/.github/workflows/pr17-go-security-fix.yml deleted file mode 100644 index d41990e9..00000000 --- a/.github/workflows/pr17-go-security-fix.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: PR17 Go Security Fix - -on: - push: - branches: [main] - -permissions: - contents: write - -concurrency: - group: pr17-go-security - cancel-in-progress: true - -jobs: - fix: - if: contains(github.event.head_commit.message, '[pr17-go-security]') - runs-on: ubuntu-latest - timeout-minutes: 10 - env: - GOPROXY: https://proxy.golang.org,direct - GOSUMDB: sum.golang.org - steps: - - uses: actions/checkout@v6 - with: - ref: fix/universal-router-diagnostics-phase1 - fetch-depth: 0 - - name: Mark start - run: | - set -euo pipefail - git config user.name 'openai-ci-fixer' - git config user.email 'openai-ci-fixer@users.noreply.github.com' - git fetch origin main - git merge --no-edit origin/main - date -u +%FT%TZ > .pr17-go-security-state - git add .pr17-go-security-state - git commit -m 'chore(ci): restart consolidated Go security remediation' - git push origin HEAD:fix/universal-router-diagnostics-phase1 - - name: Update dependencies - id: update - continue-on-error: true - shell: bash - run: | - set -o pipefail - { - set -euxo pipefail - go version - python <<'PY' - from pathlib import Path - for filename in ( - '.github/workflows/backend-ci.yml', - '.github/workflows/security-scan.yml', - '.github/workflows/release.yml', - ): - path = Path(filename) - path.write_text(path.read_text().replace('go1.26.4', 'go1.26.5')) - PY - cd backend - timeout 300s go get \ - github.com/aws/aws-sdk-go-v2/service/s3@v1.97.3 \ - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream@v1.7.8 - python - <<'PY' - from pathlib import Path - import re - path = Path('go.mod') - text = re.sub(r'(?m)^go\s+\S+$', 'go 1.26.5', path.read_text(), count=1) - path.write_text(text) - PY - cd .. - } > pr17-go-security.log 2>&1 - - name: Push result - if: always() - run: | - set -euo pipefail - if [ '${{ steps.update.outcome }}' = 'success' ]; then - rm -f .pr17-go-security-state pr17-go-security.log - git add backend/go.mod backend/go.sum \ - .github/workflows/backend-ci.yml \ - .github/workflows/security-scan.yml \ - .github/workflows/release.yml - git add -u .pr17-go-security-state - git commit -m 'fix(security): update Go and AWS SDK dependencies' - else - git add .pr17-go-security-state pr17-go-security.log - git commit -m 'chore(ci): capture Go security remediation failure' - fi - git push origin HEAD:fix/universal-router-diagnostics-phase1 diff --git a/.github/workflows/pr17-lint-fix.yml b/.github/workflows/pr17-lint-fix.yml deleted file mode 100644 index a3477268..00000000 --- a/.github/workflows/pr17-lint-fix.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: PR17 Lint Fix - -on: - push: - branches: [main] - -permissions: - contents: write - -jobs: - fix: - if: contains(github.event.head_commit.message, '[pr17-lint]') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: fix/universal-router-diagnostics-phase1 - fetch-depth: 0 - - uses: actions/setup-go@v6 - with: - go-version: '1.26.5' - - name: Patch lint failures - shell: bash - run: | - set -euo pipefail - git config user.name 'openai-ci-fixer' - git config user.email 'openai-ci-fixer@users.noreply.github.com' - git fetch origin main - git merge --no-edit origin/main - python <<'PY' - from pathlib import Path - import re - - def read(path): return Path(path).read_text() - def write(path, text): Path(path).write_text(text) - def remove_func(text, name): - match = re.search(rf'(?m)^func\s+(?:\([^\n]*\)\s+)?{re.escape(name)}\s*\(', text) - if not match: return text - start = match.start(); brace = text.find('{', match.end()) - depth = 0; i = brace; delim = None; escaped = False; line = False; block = False - while i < len(text): - ch = text[i]; nxt = text[i+1] if i+1 < len(text) else '' - if line: - if ch == '\n': line = False - elif block: - if ch == '*' and nxt == '/': block = False; i += 1 - elif delim: - if delim != '`' and escaped: escaped = False - elif delim != '`' and ch == '\\': escaped = True - elif ch == delim: delim = None - else: - if ch == '/' and nxt == '/': line = True; i += 1 - elif ch == '/' and nxt == '*': block = True; i += 1 - elif ch in ('"', "'", '`'): delim = ch - elif ch == '{': depth += 1 - elif ch == '}': - depth -= 1 - if depth == 0: - end = i + 1 - while end < len(text) and text[end] in ' \t': end += 1 - if end < len(text) and text[end] == '\n': end += 1 - return text[:start] + text[end:] - i += 1 - raise RuntimeError(name) - - path='backend/internal/handler/admin/lightbridge_connect_handler.go' - write(path, read(path).replace('\t\t\th.db.Exec(`\n\t\t\t\tUPDATE accounts','\t\t\t_, _ = h.db.Exec(`\n\t\t\t\tUPDATE accounts')) - path='backend/internal/modules/proxy/internal/mihomo/compiler_test.go' - write(path, read(path).replace( - '\tgroups := doc["proxy-groups"].([]any)\n\tgroup := groups[0].(map[string]any)', - '\tgroups, ok := doc["proxy-groups"].([]any)\n\trequire.True(t, ok)\n\trequire.NotEmpty(t, groups)\n\tgroup, ok := groups[0].(map[string]any)\n\trequire.True(t, ok)')) - for path in ('backend/internal/repository/channel_monitor_repo.go','backend/internal/repository/module_store.go'): - write(path, read(path).replace('defer rows.Close()','defer func() { _ = rows.Close() }()')) - path='backend/internal/service/gemini_native_protocol_adapter_test.go' - write(path, read(path).replace( - '\trequire.Equal(t, float64(8), resp["usageMetadata"].(map[string]any)["totalTokenCount"])', - '\tusageMetadata, ok := resp["usageMetadata"].(map[string]any)\n\trequire.True(t, ok)\n\trequire.Equal(t, float64(8), usageMetadata["totalTokenCount"])')) - path='backend/internal/service/privacy_filter.go' - write(path, read(path).replace("\t\tb.WriteString(r.Replacement)\n\t\tb.WriteByte('\\n')","\t\t_, _ = b.WriteString(r.Replacement)\n\t\t_ = b.WriteByte('\\n')")) - path='backend/internal/handler/ops_error_logger.go' - write(path, read(path).replace('ErrorBody: string(w.buf.Bytes()),','ErrorBody: w.buf.String(),')) - path='backend/internal/service/lightbridge_connect_service.go' - write(path, read(path).replace('fmt.Errorf("New API returned success=false")','fmt.Errorf("new API returned success=false")')) - dead={ - 'backend/internal/service/codex_image_generation_bridge.go':('boolOverridePtr','boolOverrideFromMap','platformBoolOverride'), - 'backend/internal/service/gateway_service.go':('billingModelForRestriction','resolveAccountUpstreamModel'), - 'backend/internal/service/model_rate_limit.go':('antigravityModelRateLimitKeys',), - 'backend/internal/service/openai_gateway_service.go':('resolveOpenAIAccountUpstreamModelForRequest',), - } - for path,names in dead.items(): - text=read(path) - for name in names: text=remove_func(text,name) - write(path,text) - PY - cd backend - gofmt -w \ - cmd/ui-theme/main.go \ - internal/handler/admin/lightbridge_connect_handler.go \ - internal/handler/admin/setting_handler.go \ - internal/handler/ops_error_logger.go \ - internal/modules/proxy/internal/mihomo/compiler_test.go \ - internal/repository/channel_monitor_repo.go \ - internal/repository/module_store.go \ - internal/repository/ui_theme_repo.go \ - internal/service/codex_image_generation_bridge.go \ - internal/service/gateway_service.go \ - internal/service/gemini_native_protocol_adapter_test.go \ - internal/service/lightbridge_connect_service.go \ - internal/service/model_rate_limit.go \ - internal/service/openai_gateway_service.go \ - internal/service/privacy_filter.go - cd .. - git add backend - git commit -m 'fix: resolve golangci-lint findings' - git push origin HEAD:fix/universal-router-diagnostics-phase1 diff --git a/.github/workflows/pr17-sheetjs-fix.yml b/.github/workflows/pr17-sheetjs-fix.yml deleted file mode 100644 index 113344a4..00000000 --- a/.github/workflows/pr17-sheetjs-fix.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: PR17 SheetJS Security Fix - -on: - push: - branches: [main] - -permissions: - contents: write - -jobs: - fix: - if: contains(github.event.head_commit.message, '[pr17-sheetjs]') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: fix/universal-router-diagnostics-phase1 - fetch-depth: 0 - - uses: pnpm/action-setup@v4 - with: - version: 9 - - uses: actions/setup-node@v6 - with: - node-version: '22.13' - cache: pnpm - cache-dependency-path: frontend/pnpm-lock.yaml - - name: Update SheetJS and audit exceptions - shell: bash - run: | - set -euo pipefail - git config user.name 'openai-ci-fixer' - git config user.email 'openai-ci-fixer@users.noreply.github.com' - git fetch origin main - git merge --no-edit origin/main - python <<'PY' - from pathlib import Path - path = Path('.github/audit-exceptions.yml') - lines = path.read_text().splitlines(True) - output = [] - i = 0 - while i < len(lines): - if lines[i].startswith(' - package: xlsx'): - i += 1 - while i < len(lines) and not lines[i].startswith(' - package:'): - i += 1 - continue - output.append(lines[i]) - i += 1 - path.write_text(''.join(output)) - PY - cd frontend - pnpm add 'xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz' - pnpm install --frozen-lockfile - cd .. - git add frontend/package.json frontend/pnpm-lock.yaml .github/audit-exceptions.yml - git commit -m 'fix(security): update SheetJS and audit exceptions' - git push origin HEAD:fix/universal-router-diagnostics-phase1 diff --git a/.github/workflows/pr17-source-fix.yml b/.github/workflows/pr17-source-fix.yml deleted file mode 100644 index af82def6..00000000 --- a/.github/workflows/pr17-source-fix.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: PR17 Source Fix - -on: - push: - branches: [main] - -permissions: - contents: write - -jobs: - fix: - if: contains(github.event.head_commit.message, '[pr17-source]') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: fix/universal-router-diagnostics-phase1 - fetch-depth: 0 - - name: Patch source contracts - shell: bash - run: | - set -euo pipefail - git config user.name 'openai-ci-fixer' - git config user.email 'openai-ci-fixer@users.noreply.github.com' - git fetch origin main - git merge --no-edit origin/main - python <<'PY' - from pathlib import Path - import re - - for filename in ( - 'backend/internal/service/token_refresh_service_test.go', - 'backend/internal/service/openai_privacy_retry_test.go', - ): - path = Path(filename) - text = path.read_text() - text = re.sub( - r'NewTokenRefreshService\(([^,\n]+), nil, nil, nil, (?!nil, )', - r'NewTokenRefreshService(\1, nil, nil, nil, nil, ', - text, - ) - path.write_text(text) - - path = Path('backend/internal/handler/admin/channel_handler_test.go') - text = path.read_text().replace( - '[]string{"anthropic", "openai", "gemini", "grok", "antigravity"}', - '[]string{"anthropic", "openai", "gemini", "antigravity"}', - ) - path.write_text(text) - - path = Path('backend/internal/server/api_contract_test.go') - text = path.read_text() - old = ( - '\t\t\t\t\t"name": "Group One",\n' - '\t\t\t\t\t"description": "desc",\n' - '\t\t\t\t\t"platform": "anthropic",' - ) - new = ( - '\t\t\t\t\t"name": "Group One",\n' - '\t\t\t\t\t"description": "desc",\n' - '\t\t\t\t\t"peak_end": "",\n' - '\t\t\t\t\t"peak_rate_enabled": false,\n' - '\t\t\t\t\t"peak_rate_multiplier": 0,\n' - '\t\t\t\t\t"peak_start": "",\n' - '\t\t\t\t\t"platform": "anthropic",' - ) - if old in text: - text = text.replace(old, new, 1) - group_start = text.index('"name": "Group One"') - group_end = text.index('\n\t\t\t\t\t}', group_start) - fragment = text[group_start:group_end] - if '"upstream_protocols": null' not in fragment: - updated = text.index('"updated_at": "2025-01-02T03:04:05Z"', group_start, group_end) - line_end = text.index('\n', updated) - line_start = text.rfind('\n', group_start, updated) + 1 - indent = text[line_start:updated] - text = text[:line_end] + ',\n' + indent + '"upstream_protocols": null' + text[line_end:] - - anchor = '\t\t\t\t\t"available_channels_enabled": false,' - replacement = ( - '\t\t\t\t\t"announcements_enabled": false,\n' - '\t\t\t\t\t"promo_enabled": false,\n' - '\t\t\t\t\t"redeem_enabled": false,\n' - + anchor - ) - text = text.replace(anchor, replacement) - - old_quota = ( - '"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},' - '"antigravity":{"daily":null,"weekly":null,"monthly":null},' - '"gemini":{"daily":null,"weekly":null,"monthly":null},' - '"openai":{"daily":null,"weekly":null,"monthly":null}},' - ) - new_quota = ( - '"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},' - '"antigravity":{"daily":null,"weekly":null,"monthly":null},' - '"custom":{"daily":null,"weekly":null,"monthly":null},' - '"gemini":{"daily":null,"weekly":null,"monthly":null},' - '"grok":{"daily":null,"weekly":null,"monthly":null},' - '"openai":{"daily":null,"weekly":null,"monthly":null}},' - ) - text = text.replace(old_quota, new_quota) - path.write_text(text) - PY - git add backend/internal/service/token_refresh_service_test.go \ - backend/internal/service/openai_privacy_retry_test.go \ - backend/internal/handler/admin/channel_handler_test.go \ - backend/internal/server/api_contract_test.go - git commit -m 'fix: repair CI contract fixtures' - git push origin HEAD:fix/universal-router-diagnostics-phase1 diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index ad2a6f99..9d9a687e 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -9,6 +9,10 @@ on: permissions: contents: read +concurrency: + group: security-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: backend-security: runs-on: ubuntu-latest @@ -23,12 +27,21 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.4' + go version | grep -q 'go1.26.5' - name: Run govulncheck working-directory: backend run: | + set -o pipefail go install golang.org/x/vuln/cmd/govulncheck@latest - govulncheck ./... + govulncheck ./... 2>&1 | tee govulncheck.log + - name: Upload backend security diagnostics + if: failure() + uses: actions/upload-artifact@v4 + with: + name: backend-security-diagnostics + path: backend/govulncheck.log + if-no-files-found: ignore + retention-days: 7 frontend-security: runs-on: ubuntu-latest diff --git a/backend/cmd/ui-theme/main.go b/backend/cmd/ui-theme/main.go index df64ca64..7be35a6b 100644 --- a/backend/cmd/ui-theme/main.go +++ b/backend/cmd/ui-theme/main.go @@ -102,7 +102,7 @@ func runApply(args []string) { var err error switch { case strings.TrimSpace(*githubURL) != "": - body, _ := json.Marshal(map[string]interface{}{"url": *githubURL, "replace": *replace}) + body, _ := json.Marshal(map[string]any{"url": *githubURL, "replace": *replace}) data, err = c.do(http.MethodPost, "/api/v1/admin/ui-themes/import-github", bytes.NewReader(body), "application/json") case strings.TrimSpace(*zipPath) != "": data, err = c.uploadZip(*zipPath, *replace) diff --git a/backend/go.mod b/backend/go.mod index e647b84d..229f256d 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,6 +1,6 @@ module github.com/WilliamWang1721/LightBridge -go 1.26.4 +go 1.26.5 require ( entgo.io/ent v0.14.5 @@ -8,10 +8,10 @@ require ( github.com/alicebob/miniredis/v2 v2.38.0 github.com/alitto/pond/v2 v2.6.2 github.com/andybalholm/brotli v1.2.0 - github.com/aws/aws-sdk-go-v2 v1.41.3 + github.com/aws/aws-sdk-go-v2 v1.42.1 github.com/aws/aws-sdk-go-v2/config v1.32.10 github.com/aws/aws-sdk-go-v2/credentials v1.19.10 - github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 + github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0 github.com/cespare/xxhash/v2 v2.3.0 github.com/coder/websocket v1.8.14 github.com/dgraph-io/ristretto v0.2.0 @@ -59,21 +59,21 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect - github.com/aws/smithy-go v1.24.2 // indirect + github.com/aws/smithy-go v1.27.3 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/bytedance/sonic v1.9.1 // indirect diff --git a/backend/go.sum b/backend/go.sum index 9e50551c..01cfe4e1 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -24,34 +24,34 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= -github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA= -github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c= +github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek= +github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 h1:3IZY0XAJquT3aHzbkHfPzy4ACPcEjVG0x87KOwtpqGY= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14/go.mod h1:zwM6veDkhGgQFqkBy+uT28AAYpLu+uFMlPl+rCg/73E= github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 h1:3GUprIsfmGcC5SACIyB0e7E0BM1O1b3Erl5CePYIAeQ= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31/go.mod h1:7PuV1yl5e2xnUbm+RqvVg5i2iBM8EyijZNoI9wsOoOc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 h1:9Fjh6fi/U5JEStVZijmaMpUwE/gvBJj7x2B/PjbO9To= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23/go.mod h1:iMoT2f1tClxrWAAnKCXjZQ6LOmfLrMG14wmnWpM+F14= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 h1:uao4A3QZ5UmB326V6KF+qRpv9Tjz7IlnlnTbbANntlU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31/go.mod h1:I/1+z0VwL1GhQyLgkoHDlygpUZ+iTAwOQ/NsftiUL2I= +github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0 h1:XptwLL+UHXgafYMIHTy59IRovLbhz3znkxY2uS/pbXU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do= github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= @@ -60,8 +60,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWA github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= -github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= -github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY= +github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= @@ -166,8 +166,6 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= diff --git a/backend/internal/handler/admin/lightbridge_connect_handler.go b/backend/internal/handler/admin/lightbridge_connect_handler.go index 4bd4b79b..44abff0a 100644 --- a/backend/internal/handler/admin/lightbridge_connect_handler.go +++ b/backend/internal/handler/admin/lightbridge_connect_handler.go @@ -81,7 +81,7 @@ func (h *LightBridgeConnectHandler) BatchBalances(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) return } - defer rows.Close() + defer func() { _ = rows.Close() }() for rows.Next() { var accountID int64 diff --git a/backend/internal/handler/auth_current_user_test.go b/backend/internal/handler/auth_current_user_test.go index 22a7f791..35546c47 100644 --- a/backend/internal/handler/auth_current_user_test.go +++ b/backend/internal/handler/auth_current_user_test.go @@ -29,19 +29,19 @@ func TestAuthHandlerGetCurrentUserReturnsProfileCompatibilityFields(t *testing.T AvatarURL: "https://cdn.example.com/linuxdo.png", AvatarSource: "remote_url", }, - identities: []service.UserAuthIdentityRecord{ - { - ProviderType: "linuxdo", - ProviderKey: "linuxdo", - ProviderSubject: "linuxdo-subject-31", - VerifiedAt: &verifiedAt, - Metadata: map[string]any{ - "username": "linuxdo-handle", - "avatar_url": "https://cdn.example.com/linuxdo.png", - }, + identities: []service.UserAuthIdentityRecord{ + { + ProviderType: "linuxdo", + ProviderKey: "linuxdo", + ProviderSubject: "linuxdo-subject-31", + VerifiedAt: &verifiedAt, + Metadata: map[string]any{ + "username": "linuxdo-handle", + "avatar_url": "https://cdn.example.com/linuxdo.png", }, }, - } + }, + } handler := &AuthHandler{ userService: service.NewUserService(repo, nil, nil, nil), diff --git a/backend/internal/repository/ui_theme_repo.go b/backend/internal/repository/ui_theme_repo.go index 5c88cb51..586db66c 100644 --- a/backend/internal/repository/ui_theme_repo.go +++ b/backend/internal/repository/ui_theme_repo.go @@ -169,7 +169,7 @@ func (r *UIThemeRepository) Delete(ctx context.Context, id string) error { } type uiThemeScanner interface { - Scan(dest ...interface{}) error + Scan(dest ...any) error } func scanUITheme(scanner uiThemeScanner) (*service.UITheme, error) { diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index 7debd29e..6156e5c8 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -344,6 +344,10 @@ func TestAPIContracts(t *testing.T) { "description": "desc", "platform": "anthropic", "rate_multiplier": 1.5, + "peak_rate_enabled": false, + "peak_start": "", + "peak_end": "", + "peak_rate_multiplier": 0, "is_exclusive": false, "status": "active", "subscription_type": "standard", diff --git a/backend/internal/service/admin_service_email_identity_sync_test.go b/backend/internal/service/admin_service_email_identity_sync_test.go index 1716371e..4b966a0e 100644 --- a/backend/internal/service/admin_service_email_identity_sync_test.go +++ b/backend/internal/service/admin_service_email_identity_sync_test.go @@ -113,8 +113,12 @@ func (s *emailSyncRepoStub) RemoveGroupFromAllowedGroups(context.Context, int64) return 0, nil } -func (s *emailSyncRepoStub) BatchSetConcurrency(context.Context, []int64, int) (int, error) { return 0, nil } -func (s *emailSyncRepoStub) BatchAddConcurrency(context.Context, []int64, int) (int, error) { return 0, nil } +func (s *emailSyncRepoStub) BatchSetConcurrency(context.Context, []int64, int) (int, error) { + return 0, nil +} +func (s *emailSyncRepoStub) BatchAddConcurrency(context.Context, []int64, int) (int, error) { + return 0, nil +} func (s *emailSyncRepoStub) AddGroupToAllowedGroups(context.Context, int64, int64) error { return nil } diff --git a/backend/internal/service/aistudio_proxy_provider.go b/backend/internal/service/aistudio_proxy_provider.go index a6bfe935..c215cf6e 100644 --- a/backend/internal/service/aistudio_proxy_provider.go +++ b/backend/internal/service/aistudio_proxy_provider.go @@ -30,10 +30,10 @@ func ProvideAistudioProxyManager(cfg *config.Config, accountService *AccountServ } return aistudio_proxy.NewManager( aistudio_proxy.Config{ - DataDir: dataDir, - RuntimeDir: runtimeDir, - PythonBin: pythonBin, - HealthTimeout: 0, // use manager default + DataDir: dataDir, + RuntimeDir: runtimeDir, + PythonBin: pythonBin, + HealthTimeout: 0, // use manager default }, &aistudioProxyCredUpdater{svc: accountService}, &aistudioProxyCredReader{svc: accountService}, diff --git a/backend/internal/service/billing_cache_service_user_platform_quota_test.go b/backend/internal/service/billing_cache_service_user_platform_quota_test.go index 870f73ef..4c70c959 100644 --- a/backend/internal/service/billing_cache_service_user_platform_quota_test.go +++ b/backend/internal/service/billing_cache_service_user_platform_quota_test.go @@ -769,9 +769,9 @@ func TestHasUserPlatformQuotaLimit(t *testing.T) { daily := 5.0 tests := []struct { - name string - setup func() *BillingCacheService - want bool + name string + setup func() *BillingCacheService + want bool }{ { name: "has_limit", diff --git a/backend/internal/service/channel_monitor_service.go b/backend/internal/service/channel_monitor_service.go index 033bcaf7..fc3bfdc6 100644 --- a/backend/internal/service/channel_monitor_service.go +++ b/backend/internal/service/channel_monitor_service.go @@ -324,7 +324,8 @@ func (s *ChannelMonitorService) GetRecentAvailability(ctx context.Context, days // ListHistory 列出某个监控最近的检测历史。 // model 为空表示返回所有模型;limit <= 0 时使用默认值,超过上限会被截断。 -func (s *ChannelMonitorService) ListHistory(ctx context.Context, id int64, model string, limit int) ([]*ChannelMonitorHistoryEntry, error) { if _, err := s.repo.GetByID(ctx, id); err != nil { +func (s *ChannelMonitorService) ListHistory(ctx context.Context, id int64, model string, limit int) ([]*ChannelMonitorHistoryEntry, error) { + if _, err := s.repo.GetByID(ctx, id); err != nil { return nil, err } if limit <= 0 { diff --git a/backend/internal/service/channel_test.go b/backend/internal/service/channel_test.go index 2f371f8a..6b4bbef8 100644 --- a/backend/internal/service/channel_test.go +++ b/backend/internal/service/channel_test.go @@ -513,7 +513,6 @@ func TestSupportedModels_WildcardExpandedFromPricing(t *testing.T) { } } - func TestSupportedModels_MissingPricingKeepsNilPricing(t *testing.T) { ch := &Channel{ ModelMapping: map[string]map[string]string{ diff --git a/backend/internal/service/claude_authenticity_passive.go b/backend/internal/service/claude_authenticity_passive.go index 60841e0d..3ca6b550 100644 --- a/backend/internal/service/claude_authenticity_passive.go +++ b/backend/internal/service/claude_authenticity_passive.go @@ -17,9 +17,9 @@ const defaultAuthenticityPassiveThreshold = 3 // thinkingSignatureState 跟踪一次流式响应中 thinking block 与其 signature 的出现情况。 // 真 Anthropic 在开启 thinking 时,每个 thinking content block 都会带 signature_delta。 type thinkingSignatureState struct { - enabled bool // 本次请求是否明确开启了 thinking + enabled bool // 本次请求是否明确开启了 thinking sawThinkingBlock bool // 是否出现 content_block_start.type == thinking - sawSignature bool // 是否出现 signature_delta 且 signature 非空 + sawSignature bool // 是否出现 signature_delta 且 signature 非空 } // isThinkingEnabledPayload 判断请求体是否明确开启了 thinking。 @@ -79,11 +79,11 @@ func (s *GatewayService) evaluateAuthenticityPassive(ctx context.Context, accoun if st.sawSignature { // 检测到合法 signature → 确认真,清零可疑计数。 updates := map[string]any{ - AccountExtraKeyAuthenticityVerdict: AuthenticityVerdictGenuine, - AccountExtraKeyAuthenticityCheckedAt: now.Format(time.RFC3339), - AccountExtraKeyAuthenticityMethod: AuthenticityMethodPassive, - AccountExtraKeyAuthenticityDetail: "valid thinking signature observed in stream", - AccountExtraKeyAuthenticitySuspicious: 0, + AccountExtraKeyAuthenticityVerdict: AuthenticityVerdictGenuine, + AccountExtraKeyAuthenticityCheckedAt: now.Format(time.RFC3339), + AccountExtraKeyAuthenticityMethod: AuthenticityMethodPassive, + AccountExtraKeyAuthenticityDetail: "valid thinking signature observed in stream", + AccountExtraKeyAuthenticitySuspicious: 0, } if err := s.accountRepo.UpdateExtra(ctx, account.ID, updates); err != nil { slog.Warn("authenticity_passive_persist_failed", "account_id", account.ID, "verdict", AuthenticityVerdictGenuine, "error", err) diff --git a/backend/internal/service/claude_authenticity_probe.go b/backend/internal/service/claude_authenticity_probe.go index dd38ca16..cdb82881 100644 --- a/backend/internal/service/claude_authenticity_probe.go +++ b/backend/internal/service/claude_authenticity_probe.go @@ -23,11 +23,11 @@ import ( // // 伪造签名通常在计费前被拒,且 max_tokens=1 兜底,单次探针成本趋近于零。 type ClaudeAuthenticityResult struct { - Verdict string `json:"verdict"` // genuine / counterfeit / unknown - Method string `json:"method"` // probe - CheckedAt time.Time `json:"checked_at"` // 检测时间 - Detail string `json:"detail,omitempty"` // 人类可读说明(错误原因/状态码等) - HTTPStatus int `json:"http_status,omitempty"` // 上游返回的状态码(便于排障) + Verdict string `json:"verdict"` // genuine / counterfeit / unknown + Method string `json:"method"` // probe + CheckedAt time.Time `json:"checked_at"` // 检测时间 + Detail string `json:"detail,omitempty"` // 人类可读说明(错误原因/状态码等) + HTTPStatus int `json:"http_status,omitempty"` // 上游返回的状态码(便于排障) } // ExtraMap 返回需要增量合并进 Account.Extra 的键值(key 级覆盖,不影响其它运行态键)。 @@ -152,7 +152,7 @@ func (s *AccountTestService) probeClaudeAuthenticity(ctx context.Context, c *gin payload := map[string]any{ "model": testModelID, "messages": []map[string]any{ - { "role": "user", "content": "hi" }, + {"role": "user", "content": "hi"}, { "role": "assistant", "content": []map[string]any{ @@ -163,7 +163,7 @@ func (s *AccountTestService) probeClaudeAuthenticity(ctx context.Context, c *gin }, }, }, - { "role": "user", "content": "go on" }, + {"role": "user", "content": "go on"}, }, "thinking": map[string]any{ "type": "enabled", diff --git a/backend/internal/service/gateway_channel_restriction_helpers_test.go b/backend/internal/service/gateway_channel_restriction_helpers_test.go new file mode 100644 index 00000000..a7bdfd7d --- /dev/null +++ b/backend/internal/service/gateway_channel_restriction_helpers_test.go @@ -0,0 +1,26 @@ +//go:build unit + +package service + +func billingModelForRestriction(source, requestedModel, channelMappedModel string) string { + switch source { + case BillingModelSourceRequested: + return requestedModel + case BillingModelSourceUpstream: + return "" + case BillingModelSourceChannelMapped: + return channelMappedModel + default: + return channelMappedModel + } +} + +func resolveAccountUpstreamModel(account *Account, requestedModel string) string { + if account == nil { + return "" + } + if account.IsAntigravity() { + return mapAntigravityModel(account, requestedModel) + } + return account.GetMappedModel(requestedModel) +} diff --git a/backend/internal/service/gemini_native_protocol_adapter_test.go b/backend/internal/service/gemini_native_protocol_adapter_test.go index 2c6a712c..bd397c9b 100644 --- a/backend/internal/service/gemini_native_protocol_adapter_test.go +++ b/backend/internal/service/gemini_native_protocol_adapter_test.go @@ -100,10 +100,24 @@ func TestWriteCapturedAnthropicAsGeminiJSON(t *testing.T) { usageMetadata, ok := resp["usageMetadata"].(map[string]any) require.True(t, ok) require.Equal(t, float64(8), usageMetadata["totalTokenCount"]) - candidates := resp["candidates"].([]any) - parts := candidates[0].(map[string]any)["content"].(map[string]any)["parts"].([]any) - require.Equal(t, "hi", parts[0].(map[string]any)["text"]) - require.Equal(t, "lookup", parts[1].(map[string]any)["functionCall"].(map[string]any)["name"]) + candidates, ok := resp["candidates"].([]any) + require.True(t, ok) + require.NotEmpty(t, candidates) + firstCandidate, ok := candidates[0].(map[string]any) + require.True(t, ok) + content, ok := firstCandidate["content"].(map[string]any) + require.True(t, ok) + parts, ok := content["parts"].([]any) + require.True(t, ok) + require.Len(t, parts, 2) + textPart, ok := parts[0].(map[string]any) + require.True(t, ok) + require.Equal(t, "hi", textPart["text"]) + toolPart, ok := parts[1].(map[string]any) + require.True(t, ok) + functionCall, ok := toolPart["functionCall"].(map[string]any) + require.True(t, ok) + require.Equal(t, "lookup", functionCall["name"]) } func TestWriteCapturedAnthropicAsGeminiStream(t *testing.T) { diff --git a/backend/internal/service/lightbridge_connect_service.go b/backend/internal/service/lightbridge_connect_service.go index 338a2f2b..6bfcdcd7 100644 --- a/backend/internal/service/lightbridge_connect_service.go +++ b/backend/internal/service/lightbridge_connect_service.go @@ -267,7 +267,7 @@ type AlertInfo struct { Type string Severity string Message string - Metadata map[string]interface{} + Metadata map[string]any } // SendAlert sends alert through configured channels @@ -297,7 +297,7 @@ func (s *LightBridgeConnectService) SendAlert(ctx context.Context, accountID int // sendWebhook sends webhook notification func (s *LightBridgeConnectService) sendWebhook(ctx context.Context, webhookURL string, accountID int64, alert *AlertInfo) error { - payload := map[string]interface{}{ + payload := map[string]any{ "account_id": accountID, "type": alert.Type, "severity": alert.Severity, diff --git a/backend/internal/service/model_catalog.go b/backend/internal/service/model_catalog.go index 29494d1b..8179696f 100644 --- a/backend/internal/service/model_catalog.go +++ b/backend/internal/service/model_catalog.go @@ -57,12 +57,12 @@ type ModelCatalogRepository interface { } type ModelCatalogService struct { - repo ModelCatalogRepository - accountRepo AccountRepository - groupRepo GroupRepository - channelService *ChannelService - monitorService *ChannelMonitorService - settingService *SettingService + repo ModelCatalogRepository + accountRepo AccountRepository + groupRepo GroupRepository + channelService *ChannelService + monitorService *ChannelMonitorService + settingService *SettingService } func NewModelCatalogService( @@ -617,10 +617,10 @@ func (s *ModelCatalogService) enrichMonitorStatus(ctx context.Context, models [] // 5. 按 model name 构建最新状态索引:modelID -> latest status type modelStatus struct { - monitorID int64 - status string - latencyMs *int - avail7d *float64 + monitorID int64 + status string + latencyMs *int + avail7d *float64 } statusByModel := make(map[string]modelStatus, len(matches)) diff --git a/backend/internal/service/openai_privacy_retry_test.go b/backend/internal/service/openai_privacy_retry_test.go index fd924099..4266b6e3 100644 --- a/backend/internal/service/openai_privacy_retry_test.go +++ b/backend/internal/service/openai_privacy_retry_test.go @@ -62,7 +62,7 @@ func TestTokenRefreshService_ensureOpenAIPrivacy_RetriesNonSuccessModes(t *testi t.Run(mode, func(t *testing.T) { t.Parallel() - service := NewTokenRefreshService(&tokenRefreshAccountRepo{}, nil, nil, nil, nil, nil, cfg, nil) + service := NewTokenRefreshService(&tokenRefreshAccountRepo{}, nil, nil, nil, nil, nil, nil, cfg, nil) privacyCalls := 0 service.SetPrivacyDeps(func(proxyURL string) (*req.Client, error) { privacyCalls++ diff --git a/backend/internal/service/privacy_filter_rules.go b/backend/internal/service/privacy_filter_rules.go index 685c4d24..f1d0fdb9 100644 --- a/backend/internal/service/privacy_filter_rules.go +++ b/backend/internal/service/privacy_filter_rules.go @@ -7,21 +7,21 @@ import ( // 隐私过滤内置规则 ID。前端用这些 ID 做 i18n 文案映射。 const ( - PrivacyFilterBuiltinEmail = "email" - PrivacyFilterBuiltinCNPhone = "cn_phone" - PrivacyFilterBuiltinIDCard = "id_card" - PrivacyFilterBuiltinBankCard = "bank_card" - PrivacyFilterBuiltinIPv4 = "ipv4" - PrivacyFilterBuiltinIPv6 = "ipv6" - PrivacyFilterBuiltinSecret = "secret" - PrivacyFilterBuiltinJWT = "jwt" + PrivacyFilterBuiltinEmail = "email" + PrivacyFilterBuiltinCNPhone = "cn_phone" + PrivacyFilterBuiltinIDCard = "id_card" + PrivacyFilterBuiltinBankCard = "bank_card" + PrivacyFilterBuiltinIPv4 = "ipv4" + PrivacyFilterBuiltinIPv6 = "ipv6" + PrivacyFilterBuiltinSecret = "secret" + PrivacyFilterBuiltinJWT = "jwt" PrivacyFilterBuiltinPrivateKey = "private_key" - PrivacyFilterBuiltinAWSKey = "aws_key" - PrivacyFilterBuiltinGitHubPAT = "github_pat" + PrivacyFilterBuiltinAWSKey = "aws_key" + PrivacyFilterBuiltinGitHubPAT = "github_pat" PrivacyFilterBuiltinSlackToken = "slack_token" PrivacyFilterBuiltinCreditCard = "credit_card" - PrivacyFilterBuiltinCNLicense = "cn_license" - PrivacyFilterBuiltinURLQuery = "url_query" + PrivacyFilterBuiltinCNLicense = "cn_license" + PrivacyFilterBuiltinURLQuery = "url_query" ) const ( diff --git a/backend/internal/service/provider_module_bridge.go b/backend/internal/service/provider_module_bridge.go index c2120f3b..ebbb6bbe 100644 --- a/backend/internal/service/provider_module_bridge.go +++ b/backend/internal/service/provider_module_bridge.go @@ -37,7 +37,7 @@ func (s *adminServiceImpl) SetProviderRegistry(registry *modules.ProviderRegistr s.providerRegistry = registry } -func (s *GatewayService) forwardModuleProvider(ctx context.Context, c *gin.Context, account *Account, parsed *ParsedRequest, startTime interface{}) (*ForwardResult, bool, error) { +func (s *GatewayService) forwardModuleProvider(ctx context.Context, c *gin.Context, account *Account, parsed *ParsedRequest, startTime any) (*ForwardResult, bool, error) { adapter, providerID, ok, err := s.resolveModuleProviderAdapter(account) if !ok || err != nil { return nil, ok, err diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index d2102b50..df05800e 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -153,11 +153,11 @@ type SystemSettings struct { AffiliateRebatePerInviteeCap float64 DefaultUserRPMLimit int DefaultSubscriptions []DefaultSubscriptionSetting - AnnouncementsEnabled bool - RedeemEnabled bool - PromoEnabled bool - ProxiesEnabled bool - ChannelPricingEnabled bool + AnnouncementsEnabled bool + RedeemEnabled bool + PromoEnabled bool + ProxiesEnabled bool + ChannelPricingEnabled bool // Model fallback configuration EnableModelFallback bool `json:"enable_model_fallback"` @@ -444,8 +444,8 @@ func DefaultRectifierSettings() *RectifierSettings { // AuthenticitySettings Claude 模型真伪检测配置。 // 控制被动 SSE 旁路检测是否启用,以及连续可疑多少次才标记假冒(避免临时降级误伤)。 type AuthenticitySettings struct { - Enabled bool `json:"enabled"` // 总开关(被动检测) - PassiveThreshold int `json:"passive_threshold"` // 连续可疑次数阈值(默认 3) + Enabled bool `json:"enabled"` // 总开关(被动检测) + PassiveThreshold int `json:"passive_threshold"` // 连续可疑次数阈值(默认 3) } // DefaultAuthenticitySettings 返回默认的真伪检测配置(启用被动检测,阈值 3)。 diff --git a/backend/internal/service/sticky_session_test.go b/backend/internal/service/sticky_session_test.go index 11ace7bd..02369b19 100644 --- a/backend/internal/service/sticky_session_test.go +++ b/backend/internal/service/sticky_session_test.go @@ -122,8 +122,8 @@ func TestShouldClearStickySession(t *testing.T) { { name: "overloaded account", account: &Account{ - Status: StatusActive, - Schedulable: true, + Status: StatusActive, + Schedulable: true, OverloadUntil: &future, }, requestedModel: "", diff --git a/backend/internal/service/token_refresh_service_test.go b/backend/internal/service/token_refresh_service_test.go index 913220da..493420fb 100644 --- a/backend/internal/service/token_refresh_service_test.go +++ b/backend/internal/service/token_refresh_service_test.go @@ -180,7 +180,7 @@ func TestTokenRefreshService_RefreshWithRetry_NilInvalidator(t *testing.T) { RetryBackoffSeconds: 0, }, } - service := NewTokenRefreshService(repo, nil, nil, nil, nil, nil, cfg, nil) + service := NewTokenRefreshService(repo, nil, nil, nil, nil, nil, nil, cfg, nil) account := &Account{ ID: 7, Platform: PlatformGemini, @@ -290,7 +290,7 @@ func TestTokenRefreshService_RefreshWithRetry_UsesCredentialsUpdater(t *testing. RetryBackoffSeconds: 0, }, } - service := NewTokenRefreshService(repo, nil, nil, nil, nil, nil, cfg, nil) + service := NewTokenRefreshService(repo, nil, nil, nil, nil, nil, nil, cfg, nil) resetAt := time.Now().Add(30 * time.Minute) account := &Account{ ID: 17, @@ -504,7 +504,7 @@ func TestTokenRefreshService_RefreshWithRetry_NoRefreshTokenDoesNotTempUnschedul RetryBackoffSeconds: 0, }, } - service := NewTokenRefreshService(repo, nil, nil, nil, nil, nil, cfg, nil) + service := NewTokenRefreshService(repo, nil, nil, nil, nil, nil, nil, cfg, nil) account := &Account{ ID: 18, Platform: PlatformOpenAI, diff --git a/backend/internal/service/ui_theme.go b/backend/internal/service/ui_theme.go index c7632ef9..04896e18 100644 --- a/backend/internal/service/ui_theme.go +++ b/backend/internal/service/ui_theme.go @@ -64,14 +64,14 @@ type UITheme struct { } type UIThemeManifest struct { - ID string `json:"id"` - Name string `json:"name"` - Version string `json:"version"` - EntryCSS string `json:"entry_css"` - Preview string `json:"preview,omitempty"` - Config []UIThemeConfigField `json:"config,omitempty"` - MenuItems []UIThemeMenuItem `json:"menu_items,omitempty"` - Meta map[string]interface{} `json:"meta,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + EntryCSS string `json:"entry_css"` + Preview string `json:"preview,omitempty"` + Config []UIThemeConfigField `json:"config,omitempty"` + MenuItems []UIThemeMenuItem `json:"menu_items,omitempty"` + Meta map[string]any `json:"meta,omitempty"` } type UIThemeConfigField struct { @@ -273,7 +273,7 @@ func (s *UIThemeService) UpdateConfig(ctx context.Context, id string, config jso if len(config) == 0 { config = json.RawMessage(`{}`) } - var obj map[string]interface{} + var obj map[string]any if err := json.Unmarshal(config, &obj); err != nil { return nil, infraerrors.BadRequest("UI_THEME_INVALID_CONFIG", "config must be a JSON object") } @@ -311,7 +311,7 @@ func (s *UIThemeService) ActiveInjection(ctx context.Context) (*UIThemeInjection if theme == nil { return nil, nil } - var config map[string]interface{} + var config map[string]any if len(theme.Config) > 0 { _ = json.Unmarshal(theme.Config, &config) } @@ -363,7 +363,7 @@ func (s *UIThemeService) applyThemeMenuItems(ctx context.Context, theme *UITheme return err } existing := parseMenuItemsForTheme(raw) - filtered := make([]map[string]interface{}, 0, len(existing)+len(manifest.MenuItems)) + filtered := make([]map[string]any, 0, len(existing)+len(manifest.MenuItems)) prefix := "theme-" + theme.ID + "-" for _, item := range existing { id, _ := item["id"].(string) @@ -403,7 +403,7 @@ func (s *UIThemeService) removeThemeMenuItems(ctx context.Context, themeID strin } existing := parseMenuItemsForTheme(raw) prefix := "theme-" + themeID + "-" - filtered := make([]map[string]interface{}, 0, len(existing)) + filtered := make([]map[string]any, 0, len(existing)) for _, item := range existing { id, _ := item["id"].(string) if !strings.HasPrefix(id, prefix) { @@ -606,13 +606,13 @@ func sanitizeThemeCSS(content []byte) ([]byte, error) { return content, nil } -func defaultUIThemeConfig(m *UIThemeManifest) map[string]interface{} { - result := make(map[string]interface{}, len(m.Config)) +func defaultUIThemeConfig(m *UIThemeManifest) map[string]any { + result := make(map[string]any, len(m.Config)) for _, field := range m.Config { if len(field.Default) == 0 { continue } - var v interface{} + var v any if err := json.Unmarshal(field.Default, &v); err == nil { result[field.Key] = v } @@ -620,7 +620,7 @@ func defaultUIThemeConfig(m *UIThemeManifest) map[string]interface{} { return result } -func uiThemeCSSVars(config map[string]interface{}) map[string]string { +func uiThemeCSSVars(config map[string]any) map[string]string { result := map[string]string{} for key, value := range config { if !uiThemeConfigKeyRegexp.MatchString(key) { @@ -716,15 +716,15 @@ func allowedThemeExt(ext string) bool { } } -func parseMenuItemsForTheme(raw string) []map[string]interface{} { - var items []map[string]interface{} +func parseMenuItemsForTheme(raw string) []map[string]any { + var items []map[string]any if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &items); err != nil { - return []map[string]interface{}{} + return []map[string]any{} } return items } -func buildThemeMenuItem(themeID string, item UIThemeMenuItem) (map[string]interface{}, error) { +func buildThemeMenuItem(themeID string, item UIThemeMenuItem) (map[string]any, error) { id := strings.TrimSpace(item.ID) if id == "" || !uiThemeIDPattern.MatchString(strings.ToLower(id)) { return nil, infraerrors.BadRequest("UI_THEME_INVALID_MENU_ITEM", "theme menu item id is invalid") @@ -759,7 +759,7 @@ func buildThemeMenuItem(themeID string, item UIThemeMenuItem) (map[string]interf default: return nil, infraerrors.BadRequest("UI_THEME_INVALID_MENU_ITEM", "theme menu item type must be markdown or iframe") } - return map[string]interface{}{ + return map[string]any{ "id": "theme-" + themeID + "-" + id, "label": label, "icon_svg": item.IconSVG, diff --git a/backend/internal/service/update_service_compat_test.go b/backend/internal/service/update_service_compat_test.go new file mode 100644 index 00000000..f4c74e45 --- /dev/null +++ b/backend/internal/service/update_service_compat_test.go @@ -0,0 +1,8 @@ +//go:build unit + +package service + +func parseVersion(version string) [3]int { + parts, _ := parseSemanticVersion(version) + return parts +} diff --git a/docs/architecture/protocol-routing-phase2.md b/docs/architecture/protocol-routing-phase2.md new file mode 100644 index 00000000..d616f368 --- /dev/null +++ b/docs/architecture/protocol-routing-phase2.md @@ -0,0 +1,23 @@ +# Phase 2 protocol routing rollout + +This phase is intentionally incremental. No routing behavior is changed until the repository baseline is green. + +## Invariants + +1. `Group.platform` remains compatibility metadata during migration. +2. Message routing decisions must eventually be based on normalized inbound protocol, account capabilities, relay mode, and available adapters. +3. Embeddings, image, realtime, files, batch, and rerank endpoints remain behind explicit capability gates until dedicated adapters exist. +4. Every routing change must include focused regression tests and must keep the full CI and Security Scan green. +5. No temporary workflow may write back to the branch or default branch. + +## Baseline gate + +Phase-two routing changes may resume only after unit tests, integration tests, frontend checks, golangci-lint, module consistency, and both Security Scan jobs pass on the same commit. + +## Rollout order + +1. Establish a green baseline and synchronize dependency lockfiles. +2. Introduce read-only protocol capability helpers and tests. +3. Migrate one endpoint family at a time. +4. Observe scheduler diagnostics and compatibility telemetry. +5. Remove legacy routing authority only after all endpoint families have capability adapters.