coding-proxy 0.2.3a1__tar.gz → 0.2.3a2__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.
Files changed (147) hide show
  1. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/PKG-INFO +1 -1
  2. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/pyproject.toml +1 -1
  3. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/server/dashboard.py +176 -39
  4. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/uv.lock +1 -1
  5. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/.github/workflows/ci.yml +0 -0
  6. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/.github/workflows/coverage.yml +0 -0
  7. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/.github/workflows/release.yml +0 -0
  8. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/.gitignore +0 -0
  9. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/AGENTS.md +0 -0
  10. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/CHANGELOG.md +0 -0
  11. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/CLAUDE.md +0 -0
  12. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/LICENSE +0 -0
  13. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/README.md +0 -0
  14. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/docs/ci-cd.md +0 -0
  15. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/docs/framework.md +0 -0
  16. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/docs/user-guide.md +0 -0
  17. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/docs/zh-CN/README.md +0 -0
  18. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/__init__.py +0 -0
  19. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/__init__.py +0 -0
  20. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/__main__.py +0 -0
  21. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/auth/__init__.py +0 -0
  22. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/auth/providers/__init__.py +0 -0
  23. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/auth/providers/base.py +0 -0
  24. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/auth/providers/github.py +0 -0
  25. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/auth/providers/google.py +0 -0
  26. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/auth/runtime.py +0 -0
  27. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/auth/store.py +0 -0
  28. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/cli/__init__.py +0 -0
  29. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/cli/auth_commands.py +0 -0
  30. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/cli/banner.py +0 -0
  31. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/compat/__init__.py +0 -0
  32. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/compat/canonical.py +0 -0
  33. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/compat/session_store.py +0 -0
  34. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/config/__init__.py +0 -0
  35. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/config/auth_schema.py +0 -0
  36. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/config/config.default.yaml +0 -0
  37. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/config/loader.py +0 -0
  38. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/config/resiliency.py +0 -0
  39. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/config/routing.py +0 -0
  40. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/config/schema.py +0 -0
  41. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/config/server.py +0 -0
  42. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/config/vendors.py +0 -0
  43. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/convert/__init__.py +0 -0
  44. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/convert/anthropic_to_gemini.py +0 -0
  45. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/convert/anthropic_to_openai.py +0 -0
  46. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/convert/gemini_sse_adapter.py +0 -0
  47. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/convert/gemini_to_anthropic.py +0 -0
  48. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/convert/openai_to_anthropic.py +0 -0
  49. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/logging/__init__.py +0 -0
  50. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/logging/db.py +0 -0
  51. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/logging/formatters.py +0 -0
  52. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/logging/stats.py +0 -0
  53. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/model/__init__.py +0 -0
  54. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/model/auth.py +0 -0
  55. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/model/compat.py +0 -0
  56. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/model/constants.py +0 -0
  57. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/model/pricing.py +0 -0
  58. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/model/token.py +0 -0
  59. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/model/vendor.py +0 -0
  60. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/pricing.py +0 -0
  61. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/routing/__init__.py +0 -0
  62. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/routing/circuit_breaker.py +0 -0
  63. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/routing/error_classifier.py +0 -0
  64. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/routing/executor.py +0 -0
  65. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/routing/model_mapper.py +0 -0
  66. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/routing/quota_guard.py +0 -0
  67. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/routing/rate_limit.py +0 -0
  68. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/routing/retry.py +0 -0
  69. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/routing/router.py +0 -0
  70. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/routing/session_manager.py +0 -0
  71. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/routing/tier.py +0 -0
  72. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/routing/usage_parser.py +0 -0
  73. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/routing/usage_recorder.py +0 -0
  74. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/server/__init__.py +0 -0
  75. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/server/app.py +0 -0
  76. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/server/factory.py +0 -0
  77. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/server/request_normalizer.py +0 -0
  78. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/server/responses.py +0 -0
  79. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/server/routes.py +0 -0
  80. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/streaming/__init__.py +0 -0
  81. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/streaming/anthropic_compat.py +0 -0
  82. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/__init__.py +0 -0
  83. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/alibaba.py +0 -0
  84. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/anthropic.py +0 -0
  85. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/antigravity.py +0 -0
  86. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/base.py +0 -0
  87. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/copilot.py +0 -0
  88. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/copilot_models.py +0 -0
  89. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/copilot_token_manager.py +0 -0
  90. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/copilot_urls.py +0 -0
  91. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/doubao.py +0 -0
  92. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/kimi.py +0 -0
  93. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/minimax.py +0 -0
  94. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/mixins.py +0 -0
  95. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/native_anthropic.py +0 -0
  96. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/token_manager.py +0 -0
  97. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/xiaomi.py +0 -0
  98. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/src/coding/proxy/vendors/zhipu.py +0 -0
  99. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/__init__.py +0 -0
  100. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_antigravity.py +0 -0
  101. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_app_routes.py +0 -0
  102. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_auto_login.py +0 -0
  103. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_banner.py +0 -0
  104. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_circuit_breaker.py +0 -0
  105. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_cli_usage.py +0 -0
  106. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_compat.py +0 -0
  107. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_config_init.py +0 -0
  108. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_config_loader.py +0 -0
  109. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_convert_request.py +0 -0
  110. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_convert_response.py +0 -0
  111. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_convert_sse.py +0 -0
  112. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_copilot.py +0 -0
  113. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_copilot_convert_request.py +0 -0
  114. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_copilot_convert_response.py +0 -0
  115. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_copilot_models.py +0 -0
  116. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_copilot_urls.py +0 -0
  117. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_currency.py +0 -0
  118. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_error_classifier.py +0 -0
  119. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_logging_dual_write.py +0 -0
  120. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_mixins.py +0 -0
  121. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_model_auth.py +0 -0
  122. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_model_compat.py +0 -0
  123. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_model_constants.py +0 -0
  124. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_model_mapper.py +0 -0
  125. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_model_pricing.py +0 -0
  126. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_model_token.py +0 -0
  127. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_model_vendor.py +0 -0
  128. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_native_vendors.py +0 -0
  129. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_parse_usage.py +0 -0
  130. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_pricing.py +0 -0
  131. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_quota_guard.py +0 -0
  132. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_rate_limit.py +0 -0
  133. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_request_normalizer.py +0 -0
  134. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_router_chain.py +0 -0
  135. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_router_executor.py +0 -0
  136. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_runtime_reauth.py +0 -0
  137. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_schema.py +0 -0
  138. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_streaming_anthropic_compat.py +0 -0
  139. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_tier.py +0 -0
  140. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_tiers_config.py +0 -0
  141. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_time_range.py +0 -0
  142. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_token_logger.py +0 -0
  143. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_token_manager.py +0 -0
  144. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_types.py +0 -0
  145. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_vendor_streaming.py +0 -0
  146. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_vendors.py +0 -0
  147. {coding_proxy-0.2.3a1 → coding_proxy-0.2.3a2}/tests/test_zhipu.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-proxy
3
- Version: 0.2.3a1
3
+ Version: 0.2.3a2
4
4
  Summary: A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM, MiniMax, Qwen, Xiaomi, Kimi, Doubao...
5
5
  Project-URL: Source Code, https://github.com/ThreeFish-AI/coding-proxy
6
6
  Project-URL: User Guide, https://github.com/ThreeFish-AI/coding-proxy/blob/master/docs/user-guide.md
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "coding-proxy"
3
- version = "0.2.3a1"
3
+ version = "0.2.3a2"
4
4
  description = "A High-Availability, Transparent, and Smart Multi-Vendor Proxy for Claude Code. Support Claude Plans, GitHub Copilot, Google Antigravity, ZAI/GLM, MiniMax, Qwen, Xiaomi, Kimi, Doubao..."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -11,6 +11,40 @@ from fastapi.responses import HTMLResponse, Response
11
11
 
12
12
  from ..logging.db import TimePeriod
13
13
 
14
+
15
+ # ── Favicon (16×16, 蓝紫渐变) ────────────────────────────────────────────
16
+ def _build_favicon() -> bytes:
17
+ """程序化生成 16×16 ICO,蓝紫渐变与 Dashboard Logo 一致."""
18
+ import struct
19
+
20
+ width, height = 16, 16
21
+ pixel_rows: list[bytes] = []
22
+ for y in range(height - 1, -1, -1): # BMP bottom-up
23
+ row = bytearray()
24
+ for x in range(width):
25
+ t = (x + (height - 1 - y)) / (width + height - 2)
26
+ r = int(88 + (188 - 88) * t)
27
+ g = int(166 + (140 - 166) * t)
28
+ b = 255
29
+ row.extend([b, g, r, 255]) # BGRA
30
+ pixel_rows.append(bytes(row))
31
+
32
+ bmp_hdr = struct.pack(
33
+ "<IIIHHIIIIII", 40, width, height * 2, 1, 32, 0, 0, 0, 0, 0, 0
34
+ )
35
+ px_data = b"".join(pixel_rows)
36
+ mask_data = b"\x00\x00\x00\x00" * height
37
+ image_data = bmp_hdr + px_data + mask_data
38
+
39
+ ico_hdr = struct.pack("<HHH", 0, 1, 1)
40
+ dir_entry = struct.pack(
41
+ "<BBBBHHII", width, height, 0, 0, 1, 32, len(image_data), 22
42
+ )
43
+ return ico_hdr + dir_entry + image_data
44
+
45
+
46
+ _FAVICON_ICO: bytes = _build_favicon()
47
+
14
48
  logger = logging.getLogger(__name__)
15
49
 
16
50
  # ── HTML 模板 ──────────────────────────────────────────────────────────────
@@ -20,7 +54,8 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
20
54
  <head>
21
55
  <meta charset="UTF-8" />
22
56
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
23
- <title>coding-proxy Dashboard</title>
57
+ <title>Coding Proxy Dashboard</title>
58
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
24
59
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
25
60
  <style>
26
61
  :root {
@@ -125,7 +160,7 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
125
160
  }
126
161
  .charts-grid-3 {
127
162
  display: grid;
128
- grid-template-columns: 1fr 1fr;
163
+ grid-template-columns: 1fr 2fr;
129
164
  gap: 14px;
130
165
  margin-bottom: 14px;
131
166
  }
@@ -197,6 +232,35 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
197
232
  border: 1px solid rgba(188,140,255,.25);
198
233
  }
199
234
  .arrow { color: var(--text-secondary); margin: 0 4px; }
235
+ /* ── 时间区间选择栏 ── */
236
+ .time-range-bar {
237
+ display: flex; align-items: center; gap: 8px;
238
+ margin-bottom: 16px; flex-wrap: wrap;
239
+ }
240
+ .time-range-label { font-size: 13px; color: var(--text-secondary); }
241
+ .range-btn {
242
+ padding: 4px 14px; border-radius: 14px;
243
+ background: rgba(48,54,61,.6);
244
+ border: 1px solid var(--border);
245
+ color: var(--text-secondary);
246
+ font-size: 12px; cursor: pointer;
247
+ transition: background .15s, color .15s, border-color .15s;
248
+ }
249
+ .range-btn:hover { background: var(--bg-card-hover); color: var(--text-primary); }
250
+ .range-btn.active {
251
+ background: rgba(88,166,255,.15);
252
+ border-color: rgba(88,166,255,.5);
253
+ color: var(--accent-blue);
254
+ }
255
+ .range-custom { display: none; align-items: center; gap: 6px; }
256
+ .range-custom.visible { display: flex; }
257
+ .range-date {
258
+ padding: 3px 8px; border-radius: var(--radius);
259
+ background: var(--bg-card); border: 1px solid var(--border);
260
+ color: var(--text-primary); font-size: 12px;
261
+ color-scheme: dark;
262
+ }
263
+ .range-sep { font-size: 12px; color: var(--text-secondary); }
200
264
  /* ── 空态 ── */
201
265
  .empty {
202
266
  text-align: center; padding: 32px;
@@ -210,7 +274,7 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
210
274
  <header>
211
275
  <div class="header-left">
212
276
  <div class="logo">C</div>
213
- <h1>coding-proxy Dashboard</h1>
277
+ <h1>Coding Proxy Dashboard</h1>
214
278
  <span class="badge" id="version-badge">v-.-.-</span>
215
279
  </div>
216
280
  <div class="header-right">
@@ -220,6 +284,19 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
220
284
  </header>
221
285
 
222
286
  <main>
287
+ <!-- 时间区间选择器 -->
288
+ <div class="time-range-bar">
289
+ <span class="time-range-label">数据时间区间:</span>
290
+ <button class="range-btn active" onclick="setTimeRange(7, this)">最近一周</button>
291
+ <button class="range-btn" onclick="setTimeRange(30, this)">最近一月</button>
292
+ <button class="range-btn" onclick="setTimeRange(0, this)">自选区间</button>
293
+ <div class="range-custom" id="range-custom">
294
+ <input type="date" id="range-start" class="range-date" onchange="applyCustomRange()" />
295
+ <span class="range-sep">–</span>
296
+ <input type="date" id="range-end" class="range-date" onchange="applyCustomRange()" />
297
+ </div>
298
+ </div>
299
+
223
300
  <!-- KPI 卡片 -->
224
301
  <div class="kpi-grid" id="kpi-grid">
225
302
  <div class="kpi-card"><div class="kpi-label">今日请求数</div><div class="kpi-value color-blue" id="kpi-req-today">–</div><div class="kpi-sub" id="kpi-req-week">本周 –</div></div>
@@ -239,25 +316,25 @@ _DASHBOARD_HTML = """<!DOCTYPE html>
239
316
  </div>
240
317
  </div>
241
318
  <div class="card">
242
- <div class="card-title">近 7 天请求量趋势</div>
319
+ <div class="card-title" id="title-timeline">近 7 天请求量趋势</div>
243
320
  <div class="chart-wrap-lg">
244
321
  <canvas id="chart-timeline"></canvas>
245
322
  </div>
246
323
  </div>
247
324
  </div>
248
325
 
249
- <!-- 供应商分布 + Token 类型分布 -->
326
+ <!-- 供应商分布 + Token 量趋势 -->
250
327
  <div class="charts-grid-3">
251
328
  <div class="card">
252
- <div class="card-title">供应商请求分布(近 7 天)</div>
329
+ <div class="card-title" id="title-vendor-dist">供应商请求分布(近 7 天)</div>
253
330
  <div class="chart-wrap">
254
331
  <canvas id="chart-vendor-dist"></canvas>
255
332
  </div>
256
333
  </div>
257
334
  <div class="card">
258
- <div class="card-title">Token 类型分布(近 7 天)</div>
259
- <div class="chart-wrap">
260
- <canvas id="chart-token-type"></canvas>
335
+ <div class="card-title" id="title-token-timeline">近 7 天 Token 量趋势</div>
336
+ <div class="chart-wrap-lg">
337
+ <canvas id="chart-token-timeline"></canvas>
261
338
  </div>
262
339
  </div>
263
340
  </div>
@@ -317,11 +394,11 @@ Chart.defaults.font.size = 12;
317
394
  // ── 图表实例 ──────────────────────────────────────────────
318
395
  let chartTimeline = null;
319
396
  let chartVendorDist = null;
320
- let chartTokenType = null;
397
+ let chartTokenTimeline = null;
321
398
 
322
399
  function destroyCharts() {
323
- [chartTimeline, chartVendorDist, chartTokenType].forEach(c => c && c.destroy());
324
- chartTimeline = chartVendorDist = chartTokenType = null;
400
+ [chartTimeline, chartVendorDist, chartTokenTimeline].forEach(c => c && c.destroy());
401
+ chartTimeline = chartVendorDist = chartTokenTimeline = null;
325
402
  }
326
403
 
327
404
  // ── 数据拉取 ──────────────────────────────────────────────
@@ -507,49 +584,52 @@ function buildVendorDist(rows) {
507
584
  });
508
585
  }
509
586
 
510
- // ── Token 类型堆叠柱图 ────────────────────────────────────
511
- function buildTokenType(rows) {
512
- // 按 date 汇总 token 类型
513
- const byDate = {};
587
+ // ── Token 量趋势折线图 ────────────────────────────────────
588
+ function buildTokenTimeline(rows) {
589
+ // 按 vendor 分组,按 date 汇总 token 总量
590
+ const vendorDateMap = {}; // vendor → {date → total_tokens}
514
591
  const allDates = new Set();
515
592
  for (const r of rows) {
516
- const d = r.date;
517
- if (!d) continue;
518
- if (!byDate[d]) byDate[d] = {input:0,output:0,cache_creation:0,cache_read:0};
519
- byDate[d].input += r.total_input || 0;
520
- byDate[d].output += r.total_output || 0;
521
- byDate[d].cache_creation += r.total_cache_creation || 0;
522
- byDate[d].cache_read += r.total_cache_read || 0;
593
+ const v = r.vendor, d = r.date;
594
+ if (!v || !d) continue;
595
+ if (!vendorDateMap[v]) vendorDateMap[v] = {};
596
+ const total = (r.total_input || 0) + (r.total_output || 0)
597
+ + (r.total_cache_creation || 0) + (r.total_cache_read || 0);
598
+ vendorDateMap[v][d] = (vendorDateMap[v][d] || 0) + total;
523
599
  allDates.add(d);
524
600
  }
525
601
  const dates = [...allDates].sort();
602
+ const vendors = Object.keys(vendorDateMap).sort();
526
603
 
527
- if (chartTokenType) chartTokenType.destroy();
528
- const ctx = document.getElementById('chart-token-type').getContext('2d');
604
+ if (chartTokenTimeline) chartTokenTimeline.destroy();
605
+ const ctx = document.getElementById('chart-token-timeline').getContext('2d');
529
606
  if (!dates.length) {
530
607
  ctx.canvas.parentElement.innerHTML = '<div class="empty">暂无数据</div>';
531
608
  return;
532
609
  }
533
610
 
534
- const typeLabels = { input:'输入', output:'输出', cache_creation:'缓存写入', cache_read:'缓存读取' };
535
- const datasets = Object.entries(typeLabels).map(([key, label]) => ({
536
- label,
537
- data: dates.map(d => byDate[d]?.[key] || 0),
538
- backgroundColor: TOKEN_COLORS[key],
539
- stack: 'tokens',
540
- borderWidth: 0,
611
+ const datasets = vendors.map((v, i) => ({
612
+ label: v,
613
+ data: dates.map(d => vendorDateMap[v][d] || 0),
614
+ borderColor: VENDOR_COLORS[i % VENDOR_COLORS.length],
615
+ backgroundColor: VENDOR_COLORS[i % VENDOR_COLORS.length] + '22',
616
+ fill: true,
617
+ tension: .3,
618
+ pointRadius: 3,
619
+ pointHoverRadius: 5,
541
620
  }));
542
621
 
543
- chartTokenType = new Chart(ctx, {
544
- type: 'bar',
622
+ chartTokenTimeline = new Chart(ctx, {
623
+ type: 'line',
545
624
  data: { labels: dates, datasets },
546
625
  options: {
547
626
  responsive: true, maintainAspectRatio: false,
548
- plugins: { legend: { position: 'bottom', labels: { boxWidth: 10, padding: 10 } } },
627
+ interaction: { mode: 'index', intersect: false },
628
+ plugins: { legend: { position: 'bottom', labels: { boxWidth: 10, padding: 12 } } },
549
629
  scales: {
550
- x: { stacked: true, grid: { color: '#30363d' } },
630
+ x: { grid: { color: '#30363d' } },
551
631
  y: {
552
- stacked: true, grid: { color: '#30363d' }, beginAtZero: true,
632
+ grid: { color: '#30363d' }, beginAtZero: true,
553
633
  ticks: { callback: v => fmtTokens(v) },
554
634
  },
555
635
  },
@@ -572,6 +652,56 @@ function updateFtTable(failoverStats) {
572
652
  </tr>`).join('');
573
653
  }
574
654
 
655
+ // ── 时间区间控制 ──────────────────────────────────────────
656
+ let currentDays = 7;
657
+
658
+ function setTimeRange(days, btn) {
659
+ currentDays = days;
660
+ document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active'));
661
+ if (btn) btn.classList.add('active');
662
+ const customEl = document.getElementById('range-custom');
663
+ if (days === 0) {
664
+ customEl.classList.add('visible');
665
+ // 初始化日期:默认今天往前 7 天
666
+ const today = new Date();
667
+ const weekAgo = new Date(today);
668
+ weekAgo.setDate(weekAgo.getDate() - 6);
669
+ document.getElementById('range-end').value = today.toISOString().slice(0, 10);
670
+ document.getElementById('range-start').value = weekAgo.toISOString().slice(0, 10);
671
+ applyCustomRange();
672
+ } else {
673
+ customEl.classList.remove('visible');
674
+ refresh();
675
+ }
676
+ }
677
+
678
+ function applyCustomRange() {
679
+ const s = document.getElementById('range-start').value;
680
+ const e = document.getElementById('range-end').value;
681
+ if (!s || !e) return;
682
+ const startMs = new Date(s).getTime();
683
+ const endMs = new Date(e).getTime();
684
+ if (endMs < startMs) return;
685
+ currentDays = Math.ceil((endMs - startMs) / 86400000) + 1;
686
+ refresh();
687
+ }
688
+
689
+ function rangeLabel() {
690
+ if (currentDays <= 7) return '近 7 天';
691
+ if (currentDays <= 30) return '近 30 天';
692
+ return '近 ' + currentDays + ' 天';
693
+ }
694
+
695
+ function updateChartTitles(days) {
696
+ const label = days <= 7 ? '近 7 天' : (days <= 30 ? '近 30 天' : '近 ' + days + ' 天');
697
+ const tl = document.getElementById('title-timeline');
698
+ const tt = document.getElementById('title-token-timeline');
699
+ const vd = document.getElementById('title-vendor-dist');
700
+ if (tl) tl.textContent = label + ' 请求量趋势';
701
+ if (tt) tt.textContent = label + ' Token 量趋势';
702
+ if (vd) vd.textContent = '供应商请求分布(' + label + ')';
703
+ }
704
+
575
705
  // ── 主刷新逻辑 ────────────────────────────────────────────
576
706
  let refreshing = false;
577
707
  async function refresh() {
@@ -579,9 +709,10 @@ async function refresh() {
579
709
  refreshing = true;
580
710
  document.getElementById('refresh-time').textContent = '刷新中…';
581
711
  try {
712
+ const days = currentDays > 0 ? currentDays : 7;
582
713
  const [summary, timeline, status] = await Promise.all([
583
714
  fetchJSON('/api/dashboard/summary'),
584
- fetchJSON('/api/dashboard/timeline?days=7'),
715
+ fetchJSON('/api/dashboard/timeline?days=' + days),
585
716
  fetchJSON('/api/status'),
586
717
  ]);
587
718
 
@@ -592,11 +723,12 @@ async function refresh() {
592
723
 
593
724
  updateKPI(summary);
594
725
  updateVendorStatus(status);
726
+ updateChartTitles(days);
595
727
 
596
728
  const rows = timeline.rows || [];
597
729
  buildTimeline(rows);
598
730
  buildVendorDist(rows);
599
- buildTokenType(rows);
731
+ buildTokenTimeline(rows);
600
732
 
601
733
  updateFtTable(summary.failover_stats || []);
602
734
 
@@ -687,6 +819,11 @@ def register_dashboard_routes(app: Any) -> None:
687
819
  """注册 Dashboard 相关路由."""
688
820
  from .. import __version__
689
821
 
822
+ @app.get("/favicon.ico", include_in_schema=False)
823
+ async def favicon() -> Response:
824
+ """返回内嵌 favicon."""
825
+ return Response(content=_FAVICON_ICO, media_type="image/x-icon")
826
+
690
827
  @app.get("/dashboard", response_class=HTMLResponse, include_in_schema=False)
691
828
  async def dashboard() -> HTMLResponse:
692
829
  """返回 Dashboard HTML 页面."""
@@ -65,7 +65,7 @@ wheels = [
65
65
 
66
66
  [[package]]
67
67
  name = "coding-proxy"
68
- version = "0.2.3a1"
68
+ version = "0.2.3a2"
69
69
  source = { editable = "." }
70
70
  dependencies = [
71
71
  { name = "aiosqlite" },
File without changes
File without changes
File without changes
File without changes