coding-proxy 0.1.0a1__tar.gz → 0.1.0a5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coding_proxy-0.1.0a5/.github/workflows/ci.yml +137 -0
- coding_proxy-0.1.0a5/.github/workflows/coverage.yml +89 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/.gitignore +7 -0
- coding_proxy-0.1.0a5/CHANGELOG.md +34 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/PKG-INFO +4 -4
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/README.md +1 -1
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/docs/user-guide.md +8 -8
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/docs/zh-CN/README.md +1 -1
- coding_proxy-0.1.0a5/pyproject.toml +89 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/__init__.py +1 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/auth/__init__.py +7 -3
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/auth/providers/github.py +1 -3
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/auth/providers/google.py +24 -18
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/auth/runtime.py +1 -1
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/auth/store.py +0 -1
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/cli/__init__.py +28 -13
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/cli/auth_commands.py +49 -21
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/compat/canonical.py +69 -46
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/compat/session_store.py +10 -4
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/config/auth_schema.py +2 -2
- coding_proxy-0.1.0a1/config.example.yaml → coding_proxy-0.1.0a5/src/coding/proxy/config/config.default.yaml +14 -4
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/config/loader.py +74 -22
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/config/resiliency.py +5 -1
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/config/routing.py +58 -25
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/config/schema.py +72 -42
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/config/server.py +1 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/config/vendors.py +6 -2
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/convert/anthropic_to_gemini.py +28 -14
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/convert/anthropic_to_openai.py +78 -43
- coding_proxy-0.1.0a5/src/coding/proxy/convert/gemini_sse_adapter.py +219 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/convert/gemini_to_anthropic.py +26 -16
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/convert/openai_to_anthropic.py +25 -11
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/logging/__init__.py +1 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/logging/db.py +61 -28
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/logging/stats.py +11 -9
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/model/__init__.py +46 -26
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/model/compat.py +6 -5
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/model/constants.py +15 -6
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/model/pricing.py +2 -2
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/model/token.py +1 -1
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/model/vendor.py +27 -10
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/routing/__init__.py +20 -9
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/routing/circuit_breaker.py +9 -6
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/routing/error_classifier.py +3 -1
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/routing/executor.py +180 -43
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/routing/model_mapper.py +17 -5
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/routing/quota_guard.py +11 -3
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/routing/rate_limit.py +4 -4
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/routing/retry.py +6 -6
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/routing/router.py +3 -2
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/routing/session_manager.py +23 -10
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/routing/tier.py +16 -5
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/routing/usage_parser.py +90 -50
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/routing/usage_recorder.py +59 -24
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/server/app.py +27 -11
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/server/factory.py +30 -14
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/server/request_normalizer.py +7 -3
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/server/responses.py +3 -1
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/server/routes.py +67 -18
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/streaming/anthropic_compat.py +195 -97
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/vendors/__init__.py +24 -14
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/vendors/anthropic.py +6 -2
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/vendors/antigravity.py +26 -10
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/vendors/base.py +62 -26
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/vendors/copilot.py +94 -30
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/vendors/copilot_models.py +19 -12
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/vendors/copilot_token_manager.py +21 -7
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/vendors/copilot_urls.py +1 -2
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/vendors/token_manager.py +7 -3
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/vendors/zhipu.py +29 -9
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_antigravity.py +62 -41
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_app_routes.py +189 -119
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_auto_login.py +91 -45
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_cli_usage.py +5 -3
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_compat.py +15 -3
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_config_loader.py +99 -27
- coding_proxy-0.1.0a5/tests/test_convert_request.py +457 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_convert_response.py +120 -87
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_convert_sse.py +209 -102
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_copilot.py +256 -107
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_copilot_convert_request.py +150 -95
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_copilot_convert_response.py +124 -76
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_copilot_models.py +25 -9
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_copilot_urls.py +26 -7
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_currency.py +2 -3
- coding_proxy-0.1.0a5/tests/test_error_classifier.py +226 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_model_compat.py +42 -14
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_model_constants.py +8 -9
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_model_mapper.py +40 -18
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_model_pricing.py +3 -1
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_model_token.py +7 -2
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_model_vendor.py +110 -51
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_parse_usage.py +109 -64
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_pricing.py +41 -20
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_quota_guard.py +1 -1
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_rate_limit.py +3 -4
- coding_proxy-0.1.0a5/tests/test_request_normalizer.py +91 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_router_chain.py +409 -214
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_router_executor.py +143 -68
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_runtime_reauth.py +11 -6
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_schema.py +50 -18
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_streaming_anthropic_compat.py +158 -77
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_tier.py +50 -10
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_tiers_config.py +19 -33
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_token_logger.py +104 -53
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_token_manager.py +4 -2
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_types.py +55 -26
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_vendor_streaming.py +60 -23
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_vendors.py +136 -72
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_zhipu.py +49 -15
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/uv.lock +128 -1
- coding_proxy-0.1.0a1/CHANGELOG.md +0 -38
- coding_proxy-0.1.0a1/pyproject.toml +0 -48
- coding_proxy-0.1.0a1/src/coding/proxy/convert/gemini_sse_adapter.py +0 -169
- coding_proxy-0.1.0a1/tests/test_convert_request.py +0 -319
- coding_proxy-0.1.0a1/tests/test_error_classifier.py +0 -176
- coding_proxy-0.1.0a1/tests/test_request_normalizer.py +0 -82
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/.github/workflows/promote.yml +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/.github/workflows/release.yml +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/AGENTS.md +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/CLAUDE.md +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/LICENSE +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/docs/ci-cd.md +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/docs/framework.md +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/__init__.py +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/__main__.py +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/auth/providers/__init__.py +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/auth/providers/base.py +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/compat/__init__.py +1 -1
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/config/__init__.py +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/convert/__init__.py +1 -1
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/model/auth.py +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/pricing.py +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/server/__init__.py +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/streaming/__init__.py +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/src/coding/proxy/vendors/mixins.py +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/__init__.py +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_circuit_breaker.py +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_mixins.py +0 -0
- {coding_proxy-0.1.0a1 → coding_proxy-0.1.0a5}/tests/test_model_auth.py +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# coding-proxy: Continuous Integration Pipeline
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Trigger:
|
|
5
|
+
# - Pull Request (所有 PR)
|
|
6
|
+
# - Push to master / feature/* 分支
|
|
7
|
+
# - Push of version tags (v*) — 确保 release 前通过质量检查
|
|
8
|
+
#
|
|
9
|
+
# Architecture: Parallel Quality Gates
|
|
10
|
+
# Job 1 (lint): Ruff 静态分析
|
|
11
|
+
# Job 2 (format): Ruff format 格式校验
|
|
12
|
+
# Job 3 (test): pytest 单元/集成测试
|
|
13
|
+
# → 三者完全并行,最大化效率
|
|
14
|
+
#
|
|
15
|
+
# References:
|
|
16
|
+
# [1] https://docs.astral.sh/ruff/integrations/#github-actions
|
|
17
|
+
# [2] https://docs.pytest.org/en/stable/how-to/continuous_integration.html
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
name: CI
|
|
21
|
+
|
|
22
|
+
on:
|
|
23
|
+
pull_request:
|
|
24
|
+
branches: [master, "feature/*"]
|
|
25
|
+
push:
|
|
26
|
+
branches: [master, "feature/*"]
|
|
27
|
+
tags: ["v*"]
|
|
28
|
+
|
|
29
|
+
permissions:
|
|
30
|
+
contents: read
|
|
31
|
+
|
|
32
|
+
env:
|
|
33
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
34
|
+
|
|
35
|
+
concurrency:
|
|
36
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
37
|
+
cancel-in-progress: true
|
|
38
|
+
|
|
39
|
+
jobs:
|
|
40
|
+
# ===========================================================================
|
|
41
|
+
# Job 1: LINT — Ruff static analysis
|
|
42
|
+
# ===========================================================================
|
|
43
|
+
lint:
|
|
44
|
+
name: Lint (Ruff)
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
timeout-minutes: 5
|
|
47
|
+
|
|
48
|
+
steps:
|
|
49
|
+
- name: Checkout repository
|
|
50
|
+
uses: actions/checkout@v4
|
|
51
|
+
with:
|
|
52
|
+
persist-credentials: false
|
|
53
|
+
|
|
54
|
+
- name: Set up uv
|
|
55
|
+
uses: astral-sh/setup-uv@v4
|
|
56
|
+
with:
|
|
57
|
+
enable-cache: true
|
|
58
|
+
|
|
59
|
+
- name: Install dependencies
|
|
60
|
+
run: uv sync --all-extras --dev
|
|
61
|
+
|
|
62
|
+
- name: Run Ruff linter
|
|
63
|
+
run: uv run ruff check .
|
|
64
|
+
|
|
65
|
+
# ===========================================================================
|
|
66
|
+
# Job 2: FORMAT — Ruff format verification
|
|
67
|
+
# ===========================================================================
|
|
68
|
+
format:
|
|
69
|
+
name: Format Check (Ruff)
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
timeout-minutes: 5
|
|
72
|
+
|
|
73
|
+
steps:
|
|
74
|
+
- name: Checkout repository
|
|
75
|
+
uses: actions/checkout@v4
|
|
76
|
+
with:
|
|
77
|
+
persist-credentials: false
|
|
78
|
+
|
|
79
|
+
- name: Set up uv
|
|
80
|
+
uses: astral-sh/setup-uv@v4
|
|
81
|
+
with:
|
|
82
|
+
enable-cache: true
|
|
83
|
+
|
|
84
|
+
- name: Install dependencies
|
|
85
|
+
run: uv sync --all-extras --dev
|
|
86
|
+
|
|
87
|
+
- name: Check formatting
|
|
88
|
+
run: uv run ruff format --check .
|
|
89
|
+
|
|
90
|
+
# ===========================================================================
|
|
91
|
+
# Job 3: TEST — Unit & integration tests
|
|
92
|
+
# ===========================================================================
|
|
93
|
+
test:
|
|
94
|
+
name: Test (pytest)
|
|
95
|
+
runs-on: ubuntu-latest
|
|
96
|
+
timeout-minutes: 10
|
|
97
|
+
strategy:
|
|
98
|
+
matrix:
|
|
99
|
+
python-version: ["3.12", "3.13"]
|
|
100
|
+
|
|
101
|
+
steps:
|
|
102
|
+
- name: Checkout repository
|
|
103
|
+
uses: actions/checkout@v4
|
|
104
|
+
with:
|
|
105
|
+
persist-credentials: false
|
|
106
|
+
|
|
107
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
108
|
+
uses: actions/setup-python@v5
|
|
109
|
+
with:
|
|
110
|
+
python-version: ${{ matrix.python-version }}
|
|
111
|
+
|
|
112
|
+
- name: Set up uv
|
|
113
|
+
uses: astral-sh/setup-uv@v4
|
|
114
|
+
with:
|
|
115
|
+
enable-cache: true
|
|
116
|
+
|
|
117
|
+
- name: Install dependencies
|
|
118
|
+
run: uv sync --all-extras --dev
|
|
119
|
+
|
|
120
|
+
- name: Run tests with coverage data collection
|
|
121
|
+
run: uv run pytest --cov=src/coding --cov-report=term-missing --cov-report=xml:coverage.xml --junitxml=junit.xml
|
|
122
|
+
|
|
123
|
+
- name: Upload coverage artifact
|
|
124
|
+
if: matrix.python-version == '3.12'
|
|
125
|
+
uses: actions/upload-artifact@v4
|
|
126
|
+
with:
|
|
127
|
+
name: coverage-data
|
|
128
|
+
path: coverage.xml
|
|
129
|
+
retention-days: 7
|
|
130
|
+
|
|
131
|
+
- name: Upload test results
|
|
132
|
+
if: always()
|
|
133
|
+
uses: actions/upload-artifact@v4
|
|
134
|
+
with:
|
|
135
|
+
name: test-results-py${{ matrix.python-version }}
|
|
136
|
+
path: junit.xml
|
|
137
|
+
retention-days: 7
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# coding-proxy: Test Coverage Reporting
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Trigger:
|
|
5
|
+
# - Pull Request (生成覆盖率报告)
|
|
6
|
+
# - Push to master (主分支更新时记录基线)
|
|
7
|
+
#
|
|
8
|
+
# Purpose:
|
|
9
|
+
# - 生成 HTML 覆盖率报告供下载查看
|
|
10
|
+
# - 在 Actions Summary 中展示覆盖率摘要
|
|
11
|
+
# - 设置最低覆盖率门槛(初始为 0,后续可逐步提高)
|
|
12
|
+
#
|
|
13
|
+
# References:
|
|
14
|
+
# [1] https://pytest-cov.readthedocs.io/
|
|
15
|
+
# [2] https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/adding-a-job-summary
|
|
16
|
+
# =============================================================================
|
|
17
|
+
|
|
18
|
+
name: Coverage
|
|
19
|
+
|
|
20
|
+
on:
|
|
21
|
+
pull_request:
|
|
22
|
+
branches: [master, "feature/*"]
|
|
23
|
+
push:
|
|
24
|
+
branches: [master]
|
|
25
|
+
|
|
26
|
+
permissions:
|
|
27
|
+
contents: read
|
|
28
|
+
|
|
29
|
+
env:
|
|
30
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
31
|
+
|
|
32
|
+
concurrency:
|
|
33
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
34
|
+
cancel-in-progress: true
|
|
35
|
+
|
|
36
|
+
jobs:
|
|
37
|
+
coverage:
|
|
38
|
+
name: Coverage Report
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
timeout-minutes: 10
|
|
41
|
+
|
|
42
|
+
steps:
|
|
43
|
+
- name: Checkout repository
|
|
44
|
+
uses: actions/checkout@v4
|
|
45
|
+
with:
|
|
46
|
+
persist-credentials: false
|
|
47
|
+
|
|
48
|
+
- name: Set up Python
|
|
49
|
+
uses: actions/setup-python@v5
|
|
50
|
+
with:
|
|
51
|
+
python-version: "3.12"
|
|
52
|
+
|
|
53
|
+
- name: Set up uv
|
|
54
|
+
uses: astral-sh/setup-uv@v4
|
|
55
|
+
with:
|
|
56
|
+
enable-cache: true
|
|
57
|
+
|
|
58
|
+
- name: Install dependencies
|
|
59
|
+
run: uv sync --all-extras --dev
|
|
60
|
+
|
|
61
|
+
- name: Generate coverage report
|
|
62
|
+
run: |
|
|
63
|
+
uv run pytest \
|
|
64
|
+
--cov=src/coding \
|
|
65
|
+
--cov-report=term-missing \
|
|
66
|
+
--cov-report=html:htmlcov \
|
|
67
|
+
--cov-report=xml:coverage.xml \
|
|
68
|
+
--cov-fail-under=0 \
|
|
69
|
+
--junitxml=junit.xml
|
|
70
|
+
|
|
71
|
+
- name: Upload HTML coverage report
|
|
72
|
+
uses: actions/upload-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
name: coverage-html-report
|
|
75
|
+
path: htmlcov/
|
|
76
|
+
retention-days: 14
|
|
77
|
+
|
|
78
|
+
- name: Upload coverage XML report
|
|
79
|
+
uses: actions/upload-artifact@v4
|
|
80
|
+
with:
|
|
81
|
+
name: coverage-xml
|
|
82
|
+
path: coverage.xml
|
|
83
|
+
retention-days: 14
|
|
84
|
+
|
|
85
|
+
- name: Display coverage summary in job summary
|
|
86
|
+
run: |
|
|
87
|
+
echo "## 📊 测试覆盖率摘要" >> $GITHUB_STEP_SUMMARY
|
|
88
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
89
|
+
uv run pytest --cov=src/coding --cov-report=term-missing --no-header -q || true
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
本文件基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/) 规范维护,版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [v0.1.0](https://github.com/ThreeFish-AI/coding-proxy/releases/tag/v0.1.0) — 2026-04-05
|
|
8
|
+
|
|
9
|
+
> [!IMPORTANT]
|
|
10
|
+
>
|
|
11
|
+
> **🎉 coding-proxy MVP 惊艳登场!**
|
|
12
|
+
>
|
|
13
|
+
> 仅需配置一行环境变量,立刻为你的 Claude Code 接入“永不宕机”的多源智能引擎。主供应商打盹?毫秒级自动无缝切换备用通道,全天候护航你的编码心流,向打断大声说不!
|
|
14
|
+
|
|
15
|
+
### ✨ 核心亮点
|
|
16
|
+
|
|
17
|
+
- **N-tier 高可用接力**:随心编排供应商优先级;默认内置 `Claude → GitHub Copilot → Antigravity → GLM` 丝滑降级链路,天塌下来有 Proxy 顶着;
|
|
18
|
+
- **自愈式智能熔断**:微秒级状态机严防“雪崩效应”,搭配指数退避重试,一旦主干回血,静默自愈切回;
|
|
19
|
+
- **账单刺客克星**:极客专属的 SQLite 本地账本 + CLI 多维看板(按维度:日/模型/供应商),把 Token 消耗拆解到每一比特,精打细算不背锅;
|
|
20
|
+
- **OAuth2 丝滑接入**:原生集成 GitHub Device Flow 与 Google OAuth。告别干枯的断更密钥,令牌到期自动接力轮转,专注写码不分心;
|
|
21
|
+
- **多协议“同传专家”**:Anthropic 与 OpenAI / Gemini 协议底层双向无损翻译,鸡同鸭讲?在 proxy 层是不存在的;
|
|
22
|
+
- **模型指名道姓**:随需定制你的神级转发地图,`claude-opus-*` 秒变 `glm-5v-turbo`,指哪打哪,模型矩阵全由你做主;
|
|
23
|
+
- **全透明“隐身衣”**:FastAPI 强劲异步驱动,开箱即用。仅需覆盖注入 `ANTHROPIC_BASE_URL`,对上层应用百分百零侵入、零违和;
|
|
24
|
+
- **SSE 星际流水线**:彻底打破协议壁垒,流式连线跨体系无损透传,体验每一颗 Token 如丝般顺滑的输出快感;
|
|
25
|
+
- **双擎配额守卫**:“5小时滑动窗口 + 固化周配额”双重护城河。余额濒临红线?主动预警机制,断然拒绝突然“断奶”;
|
|
26
|
+
|
|
27
|
+
### 🔧 更多特性
|
|
28
|
+
|
|
29
|
+
- 💰 **细粒度计价引擎**:内置主流大模型实时公开保价,调用开销追踪精确至每分每厘,资本家也薅不到你一根毛;
|
|
30
|
+
- 🔄 **强迫症级重试流**:深度可配的指数退避策略(不仅是次数,还有倍率),将偶发性异常全部静默拦截在黑盒之中;
|
|
31
|
+
- 🧠 **Vendor 降级脑图**:内置多维度供应商能力全息映射,危机时刻全自动施行“损失最小”的兼容降级路线;
|
|
32
|
+
- ⏱️ **RateLimit 算命仪**:智能嗅探并解析 Rate Limit Headers,精准算准每一秒 CD 冷却,弹无虚发;
|
|
33
|
+
- 🛡️ **神秘 421 疫苗**:专治 GitHub Copilot 偶尔抽风的著名 `421 Misdirection` 顽疾,内置“即刻重试”自愈特效药;
|
|
34
|
+
- 🧹 **洁癖级优雅退出**:挥一挥衣袖不带走一片云彩,挂起、清理、落数据,进程结束得干干净净,像风一样自由;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-proxy
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a5
|
|
4
4
|
Summary: A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM.
|
|
5
5
|
Project-URL: Source Code, https://github.com/ThreeFish-AI/coding-proxy
|
|
6
|
-
Project-URL:
|
|
7
|
-
Project-URL:
|
|
6
|
+
Project-URL: User Guide, https://github.com/ThreeFish-AI/coding-proxy/blob/master/docs/user-guide.md
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/ThreeFish-AI/coding-proxy/issues
|
|
8
8
|
Author-email: ThreeFish-AI <threefish.ai@gmail.com>
|
|
9
9
|
License-Expression: Apache-2.0
|
|
10
10
|
License-File: LICENSE
|
|
@@ -79,7 +79,7 @@ uv sync
|
|
|
79
79
|
|
|
80
80
|
### 3. Configure Keys (Using Zhipu GLM as a fallback example)
|
|
81
81
|
```bash
|
|
82
|
-
cp config.
|
|
82
|
+
cp config.default.yaml config.yaml
|
|
83
83
|
# Use environment variables to defensively inject your keys
|
|
84
84
|
export ZHIPU_API_KEY="your-api-key-here"
|
|
85
85
|
```
|
|
@@ -52,7 +52,7 @@ uv sync
|
|
|
52
52
|
|
|
53
53
|
### 3. Configure Keys (Using Zhipu GLM as a fallback example)
|
|
54
54
|
```bash
|
|
55
|
-
cp config.
|
|
55
|
+
cp config.default.yaml config.yaml
|
|
56
56
|
# Use environment variables to defensively inject your keys
|
|
57
57
|
export ZHIPU_API_KEY="your-api-key-here"
|
|
58
58
|
```
|
|
@@ -147,7 +147,7 @@ pip install -e .
|
|
|
147
147
|
|
|
148
148
|
```bash
|
|
149
149
|
# 复制配置模板到项目根目录(模板已内置完整默认值,仅需覆盖密钥)
|
|
150
|
-
cp config.
|
|
150
|
+
cp config.default.yaml config.yaml
|
|
151
151
|
```
|
|
152
152
|
|
|
153
153
|
设置智谱 API Key(二选一):
|
|
@@ -240,7 +240,7 @@ export ANTHROPIC_BASE_URL=http://127.0.0.1:8046
|
|
|
240
240
|
3. `~/.coding-proxy/config.yaml`(用户主目录)
|
|
241
241
|
4. 内置默认值(无需配置文件也可启动)
|
|
242
242
|
|
|
243
|
-
加载器会以 `config.
|
|
243
|
+
加载器会以 `config.default.yaml` 为基础模板进行深度合并,用户配置中的字段覆盖模板默认值。
|
|
244
244
|
|
|
245
245
|
### 3.2 vendors — 供应商定义
|
|
246
246
|
|
|
@@ -522,7 +522,7 @@ flowchart TD
|
|
|
522
522
|
|
|
523
523
|
**匹配优先级**:同一供应商内精确匹配 > 正则匹配(按规则顺序) > 供应商默认值
|
|
524
524
|
|
|
525
|
-
**典型配置**(基于 `config.
|
|
525
|
+
**典型配置**(基于 `config.default.yaml`):
|
|
526
526
|
|
|
527
527
|
```yaml
|
|
528
528
|
model_mapping:
|
|
@@ -678,10 +678,10 @@ auth:
|
|
|
678
678
|
如果配置文件中使用上述旧格式字段(而非 `vendors` 列表),系统会在启动时**自动迁移**至 vendors 格式并输出日志提示:
|
|
679
679
|
|
|
680
680
|
```
|
|
681
|
-
检测到旧 flat 格式配置字段,已自动迁移至 vendors 列表格式。建议迁移至 config.
|
|
681
|
+
检测到旧 flat 格式配置字段,已自动迁移至 vendors 列表格式。建议迁移至 config.default.yaml 中的 vendors 新格式。
|
|
682
682
|
```
|
|
683
683
|
|
|
684
|
-
**建议**:尽快迁移至 `config.
|
|
684
|
+
**建议**:尽快迁移至 `config.default.yaml` 中的 vendors 新格式,以获得最完整的配置能力(如 `weekly_quota_guard`、`retry`、`pricing` 等新功能仅在 vendors 格式中可用)。
|
|
685
685
|
|
|
686
686
|
> **迁移时间线**:旧 flat 格式自动迁移代码将在后续版本中移除。建议在新部署中直接使用 vendors 格式,避免依赖迁移逻辑。
|
|
687
687
|
|
|
@@ -1398,7 +1398,7 @@ coding-proxy auth reauth github
|
|
|
1398
1398
|
|
|
1399
1399
|
## 附录 B:完整配置参考
|
|
1400
1400
|
|
|
1401
|
-
> **说明**:本附录为 [`config.
|
|
1401
|
+
> **说明**:本附录为 [`config.default.yaml`](../config.default.yaml) 的精简摘要。完整注释版(含每行注释说明)请直接参阅项目根目录下的 `config.default.yaml`,它是唯一权威配置模板。
|
|
1402
1402
|
>
|
|
1403
1403
|
> 以下省略的字段在代码中均有合理默认值(参见各配置表格中的「默认值」列),无需显式配置即可生效。
|
|
1404
1404
|
|
|
@@ -1460,7 +1460,7 @@ failover:
|
|
|
1460
1460
|
# 注:FailoverConfig 代码默认值额外包含 "limit exceeded" 和 "capacity"
|
|
1461
1461
|
|
|
1462
1462
|
model_mapping:
|
|
1463
|
-
# 完整列表参见 config.
|
|
1463
|
+
# 完整列表参见 config.default.yaml 及 §3.5
|
|
1464
1464
|
- pattern: "claude-sonnet-.*"
|
|
1465
1465
|
vendors: ["copilot"]
|
|
1466
1466
|
target: "claude-sonnet-4.6"
|
|
@@ -1468,7 +1468,7 @@ model_mapping:
|
|
|
1468
1468
|
# ... 更多映射规则(按供应商分组) ...
|
|
1469
1469
|
|
|
1470
1470
|
pricing: # 可选:费用统计(四维定价 $/¥)
|
|
1471
|
-
# 完整列表参见 config.
|
|
1471
|
+
# 完整列表参见 config.default.yaml(~20 条,覆盖全部供应商与模型)
|
|
1472
1472
|
- vendor: anthropic
|
|
1473
1473
|
model: claude-sonnet-4-6
|
|
1474
1474
|
input_cost_per_mtok: $3.0
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "coding-proxy"
|
|
3
|
+
version = "0.1.0a5"
|
|
4
|
+
description = "A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
authors = [{ name = "ThreeFish-AI", email = "threefish.ai@gmail.com" }]
|
|
8
|
+
license = "Apache-2.0"
|
|
9
|
+
keywords = ["claude-code", "vibe-coding", "llm-agent", "antigravity", "github-copilot", "glm"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 5 - Production/Stable",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
dependencies = [
|
|
19
|
+
"fastapi>=0.135",
|
|
20
|
+
"uvicorn>=0.42",
|
|
21
|
+
"httpx>=0.28",
|
|
22
|
+
"pydantic>=2.12",
|
|
23
|
+
"pyyaml>=6.0",
|
|
24
|
+
"typer>=0.12.5,<0.22.0",
|
|
25
|
+
"aiosqlite>=0.22",
|
|
26
|
+
"rich>=14.3",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
"Source Code" = "https://github.com/ThreeFish-AI/coding-proxy"
|
|
31
|
+
"User Guide" = "https://github.com/ThreeFish-AI/coding-proxy/blob/master/docs/user-guide.md"
|
|
32
|
+
"Bug Tracker" = "https://github.com/ThreeFish-AI/coding-proxy/issues"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
coding-proxy = "coding.proxy.cli:app"
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["hatchling>=1.29"]
|
|
39
|
+
build-backend = "hatchling.build"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/coding"]
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = [
|
|
46
|
+
"pytest>=9.0",
|
|
47
|
+
"pytest-asyncio>=1.3",
|
|
48
|
+
"pytest-cov>=6.0",
|
|
49
|
+
"ruff>=0.9.0",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
# ── Ruff: Linter & Formatter(替代 flake8 + isort + Black + pyupgrade 等 6+ 工具) ──
|
|
53
|
+
# 文档: https://docs.astral.sh/ruff/
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
target-version = "py312"
|
|
56
|
+
line-length = 88
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = [
|
|
60
|
+
"F", # Pyflakes(未定义变量、导入错误等 — 捕获真实 Bug)
|
|
61
|
+
"E", # pycodestyle 错误
|
|
62
|
+
"W", # pycodestyle 警告
|
|
63
|
+
"I", # isort(导入排序)
|
|
64
|
+
"UP", # pyupgrade(Python 语法现代化,安全自动修复)
|
|
65
|
+
]
|
|
66
|
+
ignore = [
|
|
67
|
+
"E501", # 行过长(由 formatter 处理)
|
|
68
|
+
"E402", # 模块级非顶层导入(__init__.py 文档化分组等合理场景)
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint.isort]
|
|
72
|
+
known-first-party = ["coding"]
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint.per-file-ignores]
|
|
75
|
+
"__init__.py" = ["F401"] # __init__.py 允许未使用的导入(re-export)
|
|
76
|
+
|
|
77
|
+
[tool.ruff.format]
|
|
78
|
+
quote-style = "double"
|
|
79
|
+
indent-style = "space"
|
|
80
|
+
docstring-code-format = true
|
|
81
|
+
|
|
82
|
+
# ── Pytest 配置 ──
|
|
83
|
+
[tool.pytest.ini_options]
|
|
84
|
+
asyncio_mode = "auto"
|
|
85
|
+
testpaths = ["tests"]
|
|
86
|
+
addopts = "-v --tb=short"
|
|
87
|
+
filterwarnings = [
|
|
88
|
+
"ignore::DeprecationWarning",
|
|
89
|
+
]
|
|
@@ -7,7 +7,11 @@ from .runtime import ReauthState, RuntimeReauthCoordinator
|
|
|
7
7
|
from .store import ProviderTokens, TokenStoreManager
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
|
-
"OAuthProvider",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
10
|
+
"OAuthProvider",
|
|
11
|
+
"GitHubDeviceFlowProvider",
|
|
12
|
+
"GoogleOAuthProvider",
|
|
13
|
+
"RuntimeReauthCoordinator",
|
|
14
|
+
"ReauthState",
|
|
15
|
+
"ProviderTokens",
|
|
16
|
+
"TokenStoreManager",
|
|
13
17
|
]
|
|
@@ -6,7 +6,7 @@ import asyncio
|
|
|
6
6
|
import logging
|
|
7
7
|
import secrets
|
|
8
8
|
import time
|
|
9
|
-
from http.server import
|
|
9
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
10
10
|
from typing import Any
|
|
11
11
|
from urllib.parse import parse_qs, urlencode, urlparse
|
|
12
12
|
|
|
@@ -21,8 +21,7 @@ logger = logging.getLogger(__name__)
|
|
|
21
21
|
# SOT(权威源): coding.proxy.config.schema.AuthConfig
|
|
22
22
|
# 此处默认值仅作 fallback,生产环境应通过 config.yaml 的 auth 段覆盖
|
|
23
23
|
_DEFAULT_CLIENT_ID = (
|
|
24
|
-
"1071006060591-tmhssin2h21lcre235vtolojh4g403ep"
|
|
25
|
-
".apps.googleusercontent.com"
|
|
24
|
+
"1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
|
|
26
25
|
)
|
|
27
26
|
_DEFAULT_CLIENT_SECRET = "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"
|
|
28
27
|
|
|
@@ -44,7 +43,9 @@ class _CallbackHandler(BaseHTTPRequestHandler):
|
|
|
44
43
|
使用实例级 result dict 避免类属性在并发场景下的交叉污染.
|
|
45
44
|
"""
|
|
46
45
|
|
|
47
|
-
def __init__(
|
|
46
|
+
def __init__(
|
|
47
|
+
self, *args: Any, result: dict[str, str | None], **kwargs: Any
|
|
48
|
+
) -> None:
|
|
48
49
|
self._result = result
|
|
49
50
|
super().__init__(*args, **kwargs)
|
|
50
51
|
|
|
@@ -103,7 +104,11 @@ class GoogleOAuthProvider(OAuthProvider):
|
|
|
103
104
|
async def login(self) -> ProviderTokens:
|
|
104
105
|
"""执行 Google OAuth2 Code Flow,返回 Token."""
|
|
105
106
|
state = secrets.token_urlsafe(32)
|
|
106
|
-
result: dict[str, str | None] = {
|
|
107
|
+
result: dict[str, str | None] = {
|
|
108
|
+
"auth_code": None,
|
|
109
|
+
"state": None,
|
|
110
|
+
"error": None,
|
|
111
|
+
}
|
|
107
112
|
|
|
108
113
|
def _make_handler(*args: Any, **kwargs: Any) -> _CallbackHandler:
|
|
109
114
|
return _CallbackHandler(*args, result=result, **kwargs)
|
|
@@ -113,23 +118,26 @@ class GoogleOAuthProvider(OAuthProvider):
|
|
|
113
118
|
redirect_port = server.server_address[1]
|
|
114
119
|
redirect_uri = f"http://127.0.0.1:{redirect_port}/callback"
|
|
115
120
|
|
|
116
|
-
params = urlencode(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
params = urlencode(
|
|
122
|
+
{
|
|
123
|
+
"client_id": self._client_id,
|
|
124
|
+
"redirect_uri": redirect_uri,
|
|
125
|
+
"response_type": "code",
|
|
126
|
+
"scope": " ".join(_SCOPES),
|
|
127
|
+
"state": state,
|
|
128
|
+
"access_type": "offline",
|
|
129
|
+
"prompt": "consent",
|
|
130
|
+
}
|
|
131
|
+
)
|
|
125
132
|
auth_url = f"{_AUTH_URL}?{params}"
|
|
126
133
|
|
|
127
134
|
logger.info("请在浏览器中完成 Google 授权")
|
|
128
|
-
print(
|
|
135
|
+
print("\n 🔗 请在浏览器中访问以下链接完成授权:\n")
|
|
129
136
|
print(f" {auth_url}\n")
|
|
130
137
|
|
|
131
138
|
# 打开浏览器
|
|
132
139
|
import webbrowser
|
|
140
|
+
|
|
133
141
|
webbrowser.open(auth_url)
|
|
134
142
|
|
|
135
143
|
# 等待回调
|
|
@@ -153,9 +161,7 @@ class GoogleOAuthProvider(OAuthProvider):
|
|
|
153
161
|
# 交换 code → token
|
|
154
162
|
return await self._exchange_code(result["auth_code"], redirect_uri)
|
|
155
163
|
|
|
156
|
-
async def _exchange_code(
|
|
157
|
-
self, code: str, redirect_uri: str
|
|
158
|
-
) -> ProviderTokens:
|
|
164
|
+
async def _exchange_code(self, code: str, redirect_uri: str) -> ProviderTokens:
|
|
159
165
|
"""将 authorization code 交换为 access_token + refresh_token."""
|
|
160
166
|
resp = await self._http.post(
|
|
161
167
|
_TOKEN_URL,
|