Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cd21c570f | ||
|
|
c5e97709a5 | ||
|
|
f7adb7b28e | ||
|
|
7964fff175 |
17
README.md
17
README.md
@@ -1,10 +1,13 @@
|
|||||||
# Claude Code Router
|

|
||||||
|
|
||||||
|
[](README_zh.md)
|
||||||
|
[](https://discord.gg/rdftVMaUcS)
|
||||||
|
[](https://github.com/musistudio/claude-code-router/blob/main/LICENSE)
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
I am seeking funding support for this project to better sustain its development. If you have any ideas, feel free to reach out to me: [m@musiiot.top](mailto:m@musiiot.top)
|
I am seeking funding support for this project to better sustain its development. If you have any ideas, feel free to reach out to me: [m@musiiot.top](mailto:m@musiiot.top)
|
||||||
|
|
||||||
|
|
||||||
[中文版](README_zh.md)
|
|
||||||
|
|
||||||
> A powerful tool to route Claude Code requests to different models and customize any request.
|
> A powerful tool to route Claude Code requests to different models and customize any request.
|
||||||
|
|
||||||
> Now you can use models such as `GLM-4.5`, `Kimi-K2`, `Qwen3-Coder-480B-A35B`, and `DeepSeek v3.1` for free through the [iFlow Platform](https://platform.iflow.cn/docs/api-mode).
|
> Now you can use models such as `GLM-4.5`, `Kimi-K2`, `Qwen3-Coder-480B-A35B`, and `DeepSeek v3.1` for free through the [iFlow Platform](https://platform.iflow.cn/docs/api-mode).
|
||||||
@@ -13,6 +16,8 @@ I am seeking funding support for this project to better sustain its development.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **Model Routing**: Route requests to different models based on your needs (e.g., background tasks, thinking, long context).
|
- **Model Routing**: Route requests to different models based on your needs (e.g., background tasks, thinking, long context).
|
||||||
@@ -572,6 +577,8 @@ A huge thank you to all our sponsors for their generous support!
|
|||||||
- @\*更
|
- @\*更
|
||||||
- @\*.
|
- @\*.
|
||||||
- @F\*t
|
- @F\*t
|
||||||
|
- @\*政
|
||||||
|
- @\*铭
|
||||||
|
- @\*叶
|
||||||
|
|
||||||
(If your name is masked, please contact me via my homepage email to update it with your GitHub username.)
|
(If your name is masked, please contact me via my homepage email to update it with your GitHub username.)
|
||||||
|
|||||||
14
README_zh.md
14
README_zh.md
@@ -1,4 +1,10 @@
|
|||||||
# Claude Code Router
|

|
||||||
|
|
||||||
|
[](README.md)
|
||||||
|
[](https://discord.gg/rdftVMaUcS)
|
||||||
|
[](https://github.com/musistudio/claude-code-router/blob/main/LICENSE)
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
我正在为该项目寻求资金支持,以更好地维持其发展。如果您有任何想法,请随时与我联系: [m@musiiot.top](mailto:m@musiiot.top)
|
我正在为该项目寻求资金支持,以更好地维持其发展。如果您有任何想法,请随时与我联系: [m@musiiot.top](mailto:m@musiiot.top)
|
||||||
|
|
||||||
@@ -10,6 +16,9 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
## ✨ 功能
|
## ✨ 功能
|
||||||
|
|
||||||
- **模型路由**: 根据您的需求将请求路由到不同的模型(例如,后台任务、思考、长上下文)。
|
- **模型路由**: 根据您的需求将请求路由到不同的模型(例如,后台任务、思考、长上下文)。
|
||||||
@@ -541,6 +550,9 @@ jobs:
|
|||||||
- @\*更
|
- @\*更
|
||||||
- @\*.
|
- @\*.
|
||||||
- @F\*t
|
- @F\*t
|
||||||
|
- @\*政
|
||||||
|
- @\*铭
|
||||||
|
- @\*叶
|
||||||
|
|
||||||
(如果您的名字被屏蔽,请通过我的主页电子邮件与我联系,以便使用您的 GitHub 用户名进行更新。)
|
(如果您的名字被屏蔽,请通过我的主页电子邮件与我联系,以便使用您的 GitHub 用户名进行更新。)
|
||||||
|
|
||||||
|
|||||||
BIN
blog/images/claude-code-router-img.png
Normal file
BIN
blog/images/claude-code-router-img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
67
blog/images/roadmap.svg
Normal file
67
blog/images/roadmap.svg
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<svg viewBox="0 0 1200 420" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.road { stroke: #7aa2ff; stroke-width: 6; fill: none; filter: drop-shadow(0 6px 18px rgba(122,162,255,0.25)); }
|
||||||
|
.dash { stroke: rgba(122,162,255,0.25); stroke-width: 6; fill: none; stroke-dasharray: 2 18; }
|
||||||
|
.node { filter: drop-shadow(0 3px 10px rgba(126,240,193,0.35)); }
|
||||||
|
.node-circle { fill: #7ef0c1; }
|
||||||
|
.node-core { fill: #181b22; stroke: white; stroke-width: 1.5; }
|
||||||
|
.label-bg { fill: rgba(24,27,34,0.8); stroke: rgba(255,255,255,0.12); rx: 12; }
|
||||||
|
.label-text { fill: #e8ecf1; font-weight: 700; font-size: 14px; font-family: Arial, sans-serif; }
|
||||||
|
.label-sub { fill: #9aa6b2; font-weight: 500; font-size: 12px; font-family: Arial, sans-serif; }
|
||||||
|
.spark { fill: none; stroke: #ffd36e; stroke-width: 1.6; stroke-linecap: round; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background road with dash -->
|
||||||
|
<path class="dash" d="M60,330 C320,260 460,100 720,160 C930,205 990,260 1140,260"/>
|
||||||
|
|
||||||
|
<!-- Main road -->
|
||||||
|
<path class="road" d="M60,330 C320,260 460,100 720,160 C930,205 990,260 1140,260"/>
|
||||||
|
|
||||||
|
<!-- New Documentation Node -->
|
||||||
|
<g class="node" transform="translate(200,280)">
|
||||||
|
<circle class="node-circle" r="10"/>
|
||||||
|
<circle class="node-core" r="6"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- New Documentation Label -->
|
||||||
|
<g transform="translate(80,120)">
|
||||||
|
<rect class="label-bg" width="260" height="92"/>
|
||||||
|
<text class="label-text" x="16" y="34">New Documentation</text>
|
||||||
|
<text class="label-sub" x="16" y="58">Clear structure, examples & best practices</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Plugin Marketplace Node -->
|
||||||
|
<g class="node" transform="translate(640,150)">
|
||||||
|
<circle class="node-circle" r="10"/>
|
||||||
|
<circle class="node-core" r="6"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Plugin Marketplace Label -->
|
||||||
|
<g transform="translate(560,20)">
|
||||||
|
<rect class="label-bg" width="320" height="100"/>
|
||||||
|
<text class="label-text" x="16" y="34">Plugin Marketplace</text>
|
||||||
|
<text class="label-sub" x="16" y="58">Community submissions, ratings & version constraints</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- One More Thing Node -->
|
||||||
|
<g class="node" transform="translate(1080,255)">
|
||||||
|
<circle class="node-circle" r="10"/>
|
||||||
|
<circle class="node-core" r="6"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- One More Thing Label -->
|
||||||
|
<g transform="translate(940,300)">
|
||||||
|
<rect class="label-bg" width="250" height="86"/>
|
||||||
|
<text class="label-text" x="16" y="34">One More Thing</text>
|
||||||
|
<text class="label-sub" x="16" y="58">🚀 Confidential project · Revealing soon</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Spark decorations -->
|
||||||
|
<g transform="translate(1125,290)">
|
||||||
|
<path class="spark" d="M0 0 L8 0 M4 -4 L4 4"/>
|
||||||
|
<path class="spark" d="M14 -2 L22 -2 M18 -6 L18 2"/>
|
||||||
|
<path class="spark" d="M-10 6 L-2 6 M-6 2 L-6 10"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@musistudio/claude-code-router",
|
"name": "@musistudio/claude-code-router",
|
||||||
"version": "1.0.47",
|
"version": "1.0.49",
|
||||||
"description": "Use Claude Code without an Anthropics account and route it to another LLM provider",
|
"description": "Use Claude Code without an Anthropics account and route it to another LLM provider",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ccr": "./dist/cli.js"
|
"ccr": "./dist/cli.js"
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"@fastify/static": "^8.2.0",
|
"@fastify/static": "^8.2.0",
|
||||||
"@musistudio/llms": "^1.0.32",
|
"@musistudio/llms": "^1.0.32",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
"find-process": "^2.0.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"openurl": "^1.1.1",
|
"openurl": "^1.1.1",
|
||||||
"rotating-file-stream": "^3.2.7",
|
"rotating-file-stream": "^3.2.7",
|
||||||
|
|||||||
48
pnpm-lock.yaml
generated
48
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
|||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.4.7
|
specifier: ^16.4.7
|
||||||
version: 16.6.1
|
version: 16.6.1
|
||||||
|
find-process:
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.0.0
|
||||||
json5:
|
json5:
|
||||||
specifier: ^2.2.3
|
specifier: ^2.2.3
|
||||||
version: 2.2.3
|
version: 2.2.3
|
||||||
@@ -338,6 +341,10 @@ packages:
|
|||||||
buffer-equal-constant-time@1.0.1:
|
buffer-equal-constant-time@1.0.1:
|
||||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
|
||||||
|
chalk@4.1.2:
|
||||||
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@@ -345,6 +352,10 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
commander@12.1.0:
|
||||||
|
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
content-disposition@0.5.4:
|
content-disposition@0.5.4:
|
||||||
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -460,6 +471,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==}
|
resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
find-process@2.0.0:
|
||||||
|
resolution: {integrity: sha512-YUBQnteWGASJoEVVsOXy6XtKAY2O1FCsWnnvQ8y0YwgY1rZiKeVptnFvMu6RSELZAJOGklqseTnUGGs5D0bKmg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
foreground-child@3.3.1:
|
foreground-child@3.3.1:
|
||||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -524,6 +539,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
|
resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
has-flag@4.0.0:
|
||||||
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -609,6 +628,10 @@ packages:
|
|||||||
light-my-request@6.6.0:
|
light-my-request@6.6.0:
|
||||||
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
||||||
|
|
||||||
|
loglevel@1.9.2:
|
||||||
|
resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
|
||||||
|
engines: {node: '>= 0.6.0'}
|
||||||
|
|
||||||
lru-cache@11.1.0:
|
lru-cache@11.1.0:
|
||||||
resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
|
resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -865,6 +888,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==}
|
resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0:
|
supports-preserve-symlinks-flag@1.0.0:
|
||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1188,12 +1215,19 @@ snapshots:
|
|||||||
|
|
||||||
buffer-equal-constant-time@1.0.1: {}
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
|
|
||||||
|
chalk@4.1.2:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
supports-color: 7.2.0
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
commander@12.1.0: {}
|
||||||
|
|
||||||
content-disposition@0.5.4:
|
content-disposition@0.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
@@ -1351,6 +1385,12 @@ snapshots:
|
|||||||
fast-querystring: 1.1.2
|
fast-querystring: 1.1.2
|
||||||
safe-regex2: 5.0.0
|
safe-regex2: 5.0.0
|
||||||
|
|
||||||
|
find-process@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
chalk: 4.1.2
|
||||||
|
commander: 12.1.0
|
||||||
|
loglevel: 1.9.2
|
||||||
|
|
||||||
foreground-child@3.3.1:
|
foreground-child@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
@@ -1458,6 +1498,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
@@ -1538,6 +1580,8 @@ snapshots:
|
|||||||
process-warning: 4.0.1
|
process-warning: 4.0.1
|
||||||
set-cookie-parser: 2.7.1
|
set-cookie-parser: 2.7.1
|
||||||
|
|
||||||
|
loglevel@1.9.2: {}
|
||||||
|
|
||||||
lru-cache@11.1.0: {}
|
lru-cache@11.1.0: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
@@ -1741,6 +1785,10 @@ snapshots:
|
|||||||
|
|
||||||
strip-eof@1.0.0: {}
|
strip-eof@1.0.0: {}
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 4.0.0
|
||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0: {}
|
supports-preserve-symlinks-flag@1.0.0: {}
|
||||||
|
|
||||||
thread-stream@3.1.0:
|
thread-stream@3.1.0:
|
||||||
|
|||||||
10
src/cli.ts
10
src/cli.ts
@@ -45,7 +45,8 @@ async function waitForService(
|
|||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
while (Date.now() - startTime < timeout) {
|
while (Date.now() - startTime < timeout) {
|
||||||
if (isServiceRunning()) {
|
const isRunning = await isServiceRunning()
|
||||||
|
if (isRunning) {
|
||||||
// Wait for an additional short period to ensure service is fully ready
|
// Wait for an additional short period to ensure service is fully ready
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
return true;
|
return true;
|
||||||
@@ -56,6 +57,7 @@ async function waitForService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
const isRunning = await isServiceRunning()
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "start":
|
case "start":
|
||||||
run();
|
run();
|
||||||
@@ -95,7 +97,7 @@ async function main() {
|
|||||||
inputData += chunk;
|
inputData += chunk;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on("end", async () => {
|
process.stdin.on("end", async () => {
|
||||||
try {
|
try {
|
||||||
const input: StatusLineInput = JSON.parse(inputData);
|
const input: StatusLineInput = JSON.parse(inputData);
|
||||||
@@ -108,7 +110,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "code":
|
case "code":
|
||||||
if (!isServiceRunning()) {
|
if (!isRunning) {
|
||||||
console.log("Service not running, starting service...");
|
console.log("Service not running, starting service...");
|
||||||
const cliPath = join(__dirname, "cli.js");
|
const cliPath = join(__dirname, "cli.js");
|
||||||
const startProcess = spawn("node", [cliPath, "start"], {
|
const startProcess = spawn("node", [cliPath, "start"], {
|
||||||
@@ -153,7 +155,7 @@ async function main() {
|
|||||||
break;
|
break;
|
||||||
case "ui":
|
case "ui":
|
||||||
// Check if service is running
|
// Check if service is running
|
||||||
if (!isServiceRunning()) {
|
if (!isRunning) {
|
||||||
console.log("Service not running, starting service...");
|
console.log("Service not running, starting service...");
|
||||||
const cliPath = join(__dirname, "cli.js");
|
const cliPath = join(__dirname, "cli.js");
|
||||||
const startProcess = spawn("node", [cliPath, "start"], {
|
const startProcess = spawn("node", [cliPath, "start"], {
|
||||||
|
|||||||
17
src/index.ts
17
src/index.ts
@@ -51,7 +51,8 @@ interface RunOptions {
|
|||||||
|
|
||||||
async function run(options: RunOptions = {}) {
|
async function run(options: RunOptions = {}) {
|
||||||
// Check if service is already running
|
// Check if service is already running
|
||||||
if (isServiceRunning()) {
|
const isRunning = await isServiceRunning()
|
||||||
|
if (isRunning) {
|
||||||
console.log("✅ Service is already running in the background.");
|
console.log("✅ Service is already running in the background.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -244,7 +245,6 @@ async function run(options: RunOptions = {}) {
|
|||||||
req,
|
req,
|
||||||
config
|
config
|
||||||
});
|
});
|
||||||
console.log('result', toolResult)
|
|
||||||
toolMessages.push({
|
toolMessages.push({
|
||||||
"tool_use_id": currentToolId,
|
"tool_use_id": currentToolId,
|
||||||
"type": "tool_result",
|
"type": "tool_result",
|
||||||
@@ -295,14 +295,12 @@ async function run(options: RunOptions = {}) {
|
|||||||
|
|
||||||
// 检查流是否仍然可写
|
// 检查流是否仍然可写
|
||||||
if (!controller.desiredSize) {
|
if (!controller.desiredSize) {
|
||||||
console.log('Stream backpressure detected');
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.enqueue(value)
|
controller.enqueue(value)
|
||||||
}catch (readError: any) {
|
}catch (readError: any) {
|
||||||
if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||||
console.log('Stream reading aborted due to client disconnect');
|
|
||||||
abortController.abort(); // 中止所有相关操作
|
abortController.abort(); // 中止所有相关操作
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -318,7 +316,6 @@ async function run(options: RunOptions = {}) {
|
|||||||
|
|
||||||
// 处理流提前关闭的错误
|
// 处理流提前关闭的错误
|
||||||
if (error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
if (error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||||
console.log('Stream prematurely closed, aborting operations');
|
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -349,7 +346,7 @@ async function run(options: RunOptions = {}) {
|
|||||||
}
|
}
|
||||||
} catch (readError: any) {
|
} catch (readError: any) {
|
||||||
if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||||
console.log('Background read stream closed prematurely');
|
console.error('Background read stream closed prematurely');
|
||||||
} else {
|
} else {
|
||||||
console.error('Error in background stream reading:', readError);
|
console.error('Error in background stream reading:', readError);
|
||||||
}
|
}
|
||||||
@@ -361,6 +358,13 @@ async function run(options: RunOptions = {}) {
|
|||||||
return done(null, originalStream)
|
return done(null, originalStream)
|
||||||
}
|
}
|
||||||
sessionUsageCache.put(req.sessionId, payload.usage);
|
sessionUsageCache.put(req.sessionId, payload.usage);
|
||||||
|
if (typeof payload ==='object') {
|
||||||
|
if (payload.error) {
|
||||||
|
return done(payload.error, null)
|
||||||
|
} else {
|
||||||
|
return done(payload, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (typeof payload ==='object' && payload.error) {
|
if (typeof payload ==='object' && payload.error) {
|
||||||
return done(payload.error, null)
|
return done(payload.error, null)
|
||||||
@@ -368,7 +372,6 @@ async function run(options: RunOptions = {}) {
|
|||||||
done(null, payload)
|
done(null, payload)
|
||||||
});
|
});
|
||||||
server.addHook("onSend", async (req, reply, payload) => {
|
server.addHook("onSend", async (req, reply, payload) => {
|
||||||
console.log('主应用onSend')
|
|
||||||
event.emit('onSend', req, reply, payload);
|
event.emit('onSend', req, reply, payload);
|
||||||
return payload;
|
return payload;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { join } from 'path';
|
|||||||
|
|
||||||
export async function closeService() {
|
export async function closeService() {
|
||||||
const PID_FILE = join(HOME_DIR, '.claude-code-router.pid');
|
const PID_FILE = join(HOME_DIR, '.claude-code-router.pid');
|
||||||
|
const isRunning = await isServiceRunning()
|
||||||
if (!isServiceRunning()) {
|
|
||||||
|
if (!isRunning) {
|
||||||
console.log("No service is currently running.");
|
console.log("No service is currently running.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||||
import { PID_FILE, REFERENCE_COUNT_FILE } from '../constants';
|
import { PID_FILE, REFERENCE_COUNT_FILE } from '../constants';
|
||||||
import { readConfigFile } from '.';
|
import { readConfigFile } from '.';
|
||||||
|
import find from 'find-process';
|
||||||
|
|
||||||
|
export async function isProcessRunning(pid: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const processes = await find('pid', pid);
|
||||||
|
return processes.length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function incrementReferenceCount() {
|
export function incrementReferenceCount() {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -27,15 +37,14 @@ export function getReferenceCount(): number {
|
|||||||
return parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0;
|
return parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isServiceRunning(): boolean {
|
export async function isServiceRunning(): Promise<boolean> {
|
||||||
if (!existsSync(PID_FILE)) {
|
if (!existsSync(PID_FILE)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pid = parseInt(readFileSync(PID_FILE, 'utf-8'));
|
const pid = parseInt(readFileSync(PID_FILE, 'utf-8'));
|
||||||
process.kill(pid, 0);
|
return await isProcessRunning(pid);
|
||||||
return true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Process not running, clean up pid file
|
// Process not running, clean up pid file
|
||||||
cleanupPidFile();
|
cleanupPidFile();
|
||||||
@@ -62,7 +71,7 @@ export function getServicePid(): number | null {
|
|||||||
if (!existsSync(PID_FILE)) {
|
if (!existsSync(PID_FILE)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pid = parseInt(readFileSync(PID_FILE, 'utf-8'));
|
const pid = parseInt(readFileSync(PID_FILE, 'utf-8'));
|
||||||
return isNaN(pid) ? null : pid;
|
return isNaN(pid) ? null : pid;
|
||||||
@@ -73,10 +82,10 @@ export function getServicePid(): number | null {
|
|||||||
|
|
||||||
export async function getServiceInfo() {
|
export async function getServiceInfo() {
|
||||||
const pid = getServicePid();
|
const pid = getServicePid();
|
||||||
const running = isServiceRunning();
|
const running = await isServiceRunning();
|
||||||
const config = await readConfigFile();
|
const config = await readConfigFile();
|
||||||
const port = config.PORT || 3456;
|
const port = config.PORT || 3456;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
running,
|
running,
|
||||||
pid,
|
pid,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "@anthropic-ai/sdk/resources/messages";
|
} from "@anthropic-ai/sdk/resources/messages";
|
||||||
import { get_encoding } from "tiktoken";
|
import { get_encoding } from "tiktoken";
|
||||||
import { sessionUsageCache, Usage } from "./cache";
|
import { sessionUsageCache, Usage } from "./cache";
|
||||||
|
import { readFile } from 'fs/promises'
|
||||||
|
|
||||||
const enc = get_encoding("cl100k_base");
|
const enc = get_encoding("cl100k_base");
|
||||||
|
|
||||||
@@ -147,6 +148,11 @@ export const router = async (req: any, _res: any, context: any) => {
|
|||||||
}
|
}
|
||||||
const lastMessageUsage = sessionUsageCache.get(req.sessionId);
|
const lastMessageUsage = sessionUsageCache.get(req.sessionId);
|
||||||
const { messages, system = [], tools }: MessageCreateParamsBase = req.body;
|
const { messages, system = [], tools }: MessageCreateParamsBase = req.body;
|
||||||
|
if (config.REWRITE_SYSTEM_PROMPT && system.length > 1 && system[1]?.text?.includes('<env>')) {
|
||||||
|
const prompt = await readFile(config.REWRITE_SYSTEM_PROMPT, 'utf-8');
|
||||||
|
system[1].text = `${prompt}<env>${system[1].text.split('<env>').pop()}`
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenCount = calculateTokenCount(
|
const tokenCount = calculateTokenCount(
|
||||||
messages as MessageParam[],
|
messages as MessageParam[],
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
"@radix-ui/react-popover": "^1.1.14",
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
102
ui/pnpm-lock.yaml
generated
102
ui/pnpm-lock.yaml
generated
@@ -26,9 +26,6 @@ importers:
|
|||||||
'@radix-ui/react-switch':
|
'@radix-ui/react-switch':
|
||||||
specifier: ^1.2.5
|
specifier: ^1.2.5
|
||||||
version: 1.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-tabs':
|
|
||||||
specifier: ^1.1.13
|
|
||||||
version: 1.1.13(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
|
||||||
'@radix-ui/react-tooltip':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: ^1.2.7
|
specifier: ^1.2.7
|
||||||
version: 1.2.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.2.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@@ -520,19 +517,6 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-collection@1.1.7':
|
|
||||||
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
'@types/react-dom': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-compose-refs@1.1.2':
|
'@radix-ui/react-compose-refs@1.1.2':
|
||||||
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -564,15 +548,6 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-direction@1.1.1':
|
|
||||||
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-dismissable-layer@1.1.10':
|
'@radix-ui/react-dismissable-layer@1.1.10':
|
||||||
resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==}
|
resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -734,19 +709,6 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-roving-focus@1.1.11':
|
|
||||||
resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
'@types/react-dom': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.2.3':
|
'@radix-ui/react-slot@1.2.3':
|
||||||
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -769,19 +731,6 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-tabs@1.1.13':
|
|
||||||
resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
'@types/react-dom': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-tooltip@1.2.8':
|
'@radix-ui/react-tooltip@1.2.8':
|
||||||
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2384,18 +2333,6 @@ snapshots:
|
|||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
'@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
|
||||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
|
||||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
|
||||||
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0)
|
|
||||||
react: 19.1.0
|
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 19.1.8
|
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
|
||||||
|
|
||||||
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.8)(react@19.1.0)':
|
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.8)(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@@ -2430,12 +2367,6 @@ snapshots:
|
|||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
'@radix-ui/react-direction@1.1.1(@types/react@19.1.8)(react@19.1.0)':
|
|
||||||
dependencies:
|
|
||||||
react: 19.1.0
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 19.1.8
|
|
||||||
|
|
||||||
'@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.2
|
'@radix-ui/primitive': 1.1.2
|
||||||
@@ -2593,23 +2524,6 @@ snapshots:
|
|||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
'@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/primitive': 1.1.3
|
|
||||||
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
|
||||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
|
||||||
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.0)
|
|
||||||
'@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
|
|
||||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0)
|
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
|
|
||||||
react: 19.1.0
|
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 19.1.8
|
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.2.3(@types/react@19.1.8)(react@19.1.0)':
|
'@radix-ui/react-slot@1.2.3(@types/react@19.1.8)(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
||||||
@@ -2632,22 +2546,6 @@ snapshots:
|
|||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
'@radix-ui/react-tabs@1.1.13(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/primitive': 1.1.3
|
|
||||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
|
||||||
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.0)
|
|
||||||
'@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
|
|
||||||
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
|
||||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
|
||||||
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
|
|
||||||
react: 19.1.0
|
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 19.1.8
|
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
|
||||||
|
|
||||||
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
|||||||
@@ -1,496 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { ArrowLeft, Send, Copy, Square, History, Maximize } from 'lucide-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import MonacoEditor from '@monaco-editor/react';
|
|
||||||
import { RequestHistoryDrawer } from './RequestHistoryDrawer';
|
|
||||||
import { requestHistoryDB } from '@/lib/db';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
||||||
|
|
||||||
export function DebugPage() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
const [requestData, setRequestData] = useState({
|
|
||||||
url: '',
|
|
||||||
method: 'POST',
|
|
||||||
headers: '{}',
|
|
||||||
body: '{}'
|
|
||||||
});
|
|
||||||
const [responseData, setResponseData] = useState({
|
|
||||||
status: 0,
|
|
||||||
responseTime: 0,
|
|
||||||
body: '',
|
|
||||||
headers: '{}'
|
|
||||||
});
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [isHistoryDrawerOpen, setIsHistoryDrawerOpen] = useState(false);
|
|
||||||
const [fullscreenEditor, setFullscreenEditor] = useState<'headers' | 'body' | null>(null);
|
|
||||||
const headersEditorRef = useRef<any>(null);
|
|
||||||
const bodyEditorRef = useRef<any>(null);
|
|
||||||
|
|
||||||
// 切换全屏模式
|
|
||||||
const toggleFullscreen = (editorType: 'headers' | 'body') => {
|
|
||||||
const isEnteringFullscreen = fullscreenEditor !== editorType;
|
|
||||||
setFullscreenEditor(isEnteringFullscreen ? editorType : null);
|
|
||||||
|
|
||||||
// 延迟触发Monaco编辑器的重新布局,等待DOM更新完成
|
|
||||||
setTimeout(() => {
|
|
||||||
if (headersEditorRef.current) {
|
|
||||||
headersEditorRef.current.layout();
|
|
||||||
}
|
|
||||||
if (bodyEditorRef.current) {
|
|
||||||
bodyEditorRef.current.layout();
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从URL参数中解析日志数据
|
|
||||||
useEffect(() => {
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
const logDataParam = params.get('logData');
|
|
||||||
|
|
||||||
if (logDataParam) {
|
|
||||||
try {
|
|
||||||
const parsedData = JSON.parse(decodeURIComponent(logDataParam));
|
|
||||||
|
|
||||||
// 解析URL - 支持多种字段名
|
|
||||||
const url = parsedData.url || parsedData.requestUrl || parsedData.endpoint || '';
|
|
||||||
|
|
||||||
// 解析Method - 支持多种字段名和大小写
|
|
||||||
const method = (parsedData.method || parsedData.requestMethod || 'POST').toUpperCase();
|
|
||||||
|
|
||||||
// 解析Headers - 支持多种格式
|
|
||||||
let headers: Record<string, string> = {};
|
|
||||||
if (parsedData.headers) {
|
|
||||||
if (typeof parsedData.headers === 'string') {
|
|
||||||
try {
|
|
||||||
headers = JSON.parse(parsedData.headers);
|
|
||||||
} catch {
|
|
||||||
// 如果是字符串格式,尝试解析为键值对
|
|
||||||
const headerLines = parsedData.headers.split('\n');
|
|
||||||
headerLines.forEach((line: string) => {
|
|
||||||
const [key, ...values] = line.split(':');
|
|
||||||
if (key && values.length > 0) {
|
|
||||||
headers[key.trim()] = values.join(':').trim();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
headers = parsedData.headers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析Body - 支持多种格式和嵌套结构
|
|
||||||
let body: Record<string, unknown> = {};
|
|
||||||
let bodyData = null;
|
|
||||||
|
|
||||||
// 支持多种字段名和嵌套结构
|
|
||||||
if (parsedData.body) {
|
|
||||||
bodyData = parsedData.body;
|
|
||||||
} else if (parsedData.request && parsedData.request.body) {
|
|
||||||
bodyData = parsedData.request.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bodyData) {
|
|
||||||
if (typeof bodyData === 'string') {
|
|
||||||
try {
|
|
||||||
// 尝试解析为JSON对象
|
|
||||||
const parsed = JSON.parse(bodyData);
|
|
||||||
body = parsed;
|
|
||||||
} catch {
|
|
||||||
// 如果不是JSON,检查是否是纯文本
|
|
||||||
const trimmed = bodyData.trim();
|
|
||||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
||||||
// 看起来像JSON但解析失败,作为字符串保存
|
|
||||||
body = { raw: bodyData };
|
|
||||||
} else {
|
|
||||||
// 普通文本,直接保存
|
|
||||||
body = { content: bodyData };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (typeof bodyData === 'object') {
|
|
||||||
// 已经是对象,直接使用
|
|
||||||
body = bodyData;
|
|
||||||
} else {
|
|
||||||
// 其他类型,转换为字符串
|
|
||||||
body = { content: String(bodyData) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预填充请求表单
|
|
||||||
setRequestData({
|
|
||||||
url,
|
|
||||||
method,
|
|
||||||
headers: JSON.stringify(headers, null, 2),
|
|
||||||
body: JSON.stringify(body, null, 2)
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Log data parsed successfully:', { url, method, headers, body });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to parse log data:', error);
|
|
||||||
console.error('Raw log data:', logDataParam);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [location.search]);
|
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
const sendRequest = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const headers = JSON.parse(requestData.headers);
|
|
||||||
const body = JSON.parse(requestData.body);
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const response = await fetch(requestData.url, {
|
|
||||||
method: requestData.method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...headers
|
|
||||||
},
|
|
||||||
body: requestData.method !== 'GET' ? JSON.stringify(body) : undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
const endTime = Date.now();
|
|
||||||
const responseTime = endTime - startTime;
|
|
||||||
|
|
||||||
const responseHeaders: Record<string, string> = {};
|
|
||||||
response.headers.forEach((value, key) => {
|
|
||||||
responseHeaders[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseText = await response.text();
|
|
||||||
let responseBody = responseText;
|
|
||||||
|
|
||||||
// 尝试解析JSON响应
|
|
||||||
try {
|
|
||||||
const jsonResponse = JSON.parse(responseText);
|
|
||||||
responseBody = JSON.stringify(jsonResponse, null, 2);
|
|
||||||
} catch {
|
|
||||||
// 如果不是JSON,保持原样
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseHeadersString = JSON.stringify(responseHeaders, null, 2);
|
|
||||||
|
|
||||||
setResponseData({
|
|
||||||
status: response.status,
|
|
||||||
responseTime,
|
|
||||||
body: responseBody,
|
|
||||||
headers: responseHeadersString
|
|
||||||
});
|
|
||||||
|
|
||||||
// 保存到IndexedDB
|
|
||||||
await requestHistoryDB.saveRequest({
|
|
||||||
url: requestData.url,
|
|
||||||
method: requestData.method,
|
|
||||||
headers: requestData.headers,
|
|
||||||
body: requestData.body,
|
|
||||||
status: response.status,
|
|
||||||
responseTime,
|
|
||||||
responseBody,
|
|
||||||
responseHeaders: responseHeadersString
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Request failed:', error);
|
|
||||||
setResponseData({
|
|
||||||
status: 0,
|
|
||||||
responseTime: 0,
|
|
||||||
body: `请求失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
|
||||||
headers: '{}'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从历史记录中选择请求
|
|
||||||
const handleSelectRequest = (request: import('@/lib/db').RequestHistoryItem) => {
|
|
||||||
setRequestData({
|
|
||||||
url: request.url,
|
|
||||||
method: request.method,
|
|
||||||
headers: request.headers,
|
|
||||||
body: request.body
|
|
||||||
});
|
|
||||||
|
|
||||||
setResponseData({
|
|
||||||
status: request.status,
|
|
||||||
responseTime: request.responseTime,
|
|
||||||
body: request.responseBody,
|
|
||||||
headers: request.responseHeaders
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 复制cURL命令
|
|
||||||
const copyCurl = () => {
|
|
||||||
try {
|
|
||||||
const headers = JSON.parse(requestData.headers);
|
|
||||||
const body = JSON.parse(requestData.body);
|
|
||||||
|
|
||||||
let curlCommand = `curl -X ${requestData.method} "${requestData.url}"`;
|
|
||||||
|
|
||||||
// 添加headers
|
|
||||||
Object.entries(headers).forEach(([key, value]) => {
|
|
||||||
curlCommand += ` \\\n -H "${key}: ${value}"`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加body
|
|
||||||
if (requestData.method !== 'GET' && Object.keys(body).length > 0) {
|
|
||||||
curlCommand += ` \\\n -d '${JSON.stringify(body)}'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(curlCommand);
|
|
||||||
alert('cURL命令已复制到剪贴板');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to copy cURL:', error);
|
|
||||||
alert('复制cURL命令失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-screen bg-gray-50 font-sans">
|
|
||||||
{/* 头部 */}
|
|
||||||
<header className="flex h-16 items-center justify-between border-b bg-white px-6">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => navigate('/dashboard')}>
|
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
||||||
返回
|
|
||||||
</Button>
|
|
||||||
<h1 className="text-xl font-semibold text-gray-800">HTTP 调试器</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="outline" onClick={() => setIsHistoryDrawerOpen(true)}>
|
|
||||||
<History className="h-4 w-4 mr-2" />
|
|
||||||
历史记录
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" onClick={copyCurl}>
|
|
||||||
<Copy className="h-4 w-4 mr-2" />
|
|
||||||
复制 cURL
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* 主要内容 */}
|
|
||||||
<main className="flex h-[calc(100vh-4rem)] flex-col gap-4 p-4 overflow-hidden">
|
|
||||||
{/* 上部分:请求参数配置 - 上中下布局 */}
|
|
||||||
<div className="h-1/2 flex flex-col gap-4">
|
|
||||||
<div className="bg-white rounded-lg border p-4 flex-1 flex flex-col">
|
|
||||||
<h3 className="font-medium mb-4">请求参数配置</h3>
|
|
||||||
<div className="flex flex-col gap-4 flex-1">
|
|
||||||
{/* 上:Method、URL和发送请求按钮配置 */}
|
|
||||||
<div className="flex gap-4 items-end">
|
|
||||||
<div className="w-32">
|
|
||||||
<label className="block text-sm font-medium mb-1">Method</label>
|
|
||||||
<select
|
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
|
||||||
value={requestData.method}
|
|
||||||
onChange={(e) => setRequestData(prev => ({ ...prev, method: e.target.value }))}
|
|
||||||
>
|
|
||||||
<option value="GET">GET</option>
|
|
||||||
<option value="POST">POST</option>
|
|
||||||
<option value="PUT">PUT</option>
|
|
||||||
<option value="DELETE">DELETE</option>
|
|
||||||
<option value="PATCH">PATCH</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<label className="block text-sm font-medium mb-1">URL</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
|
||||||
value={requestData.url}
|
|
||||||
onChange={(e) => setRequestData(prev => ({ ...prev, url: e.target.value }))}
|
|
||||||
placeholder="https://api.example.com/endpoint"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant={isLoading ? "destructive" : "default"}
|
|
||||||
onClick={isLoading ? () => {} : sendRequest}
|
|
||||||
disabled={isLoading || !requestData.url.trim()}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Square className="h-4 w-4 mr-2" />
|
|
||||||
请求中...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Send className="h-4 w-4 mr-2" />
|
|
||||||
发送请求
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Headers和Body配置 - 使用tab布局 */}
|
|
||||||
<div className="flex-1">
|
|
||||||
<Tabs defaultValue="headers" className="h-full flex flex-col">
|
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
|
||||||
<TabsTrigger value="headers">Headers</TabsTrigger>
|
|
||||||
<TabsTrigger value="body">Body</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="headers" className="flex-1 mt-2">
|
|
||||||
<div
|
|
||||||
className={`${fullscreenEditor === 'headers' ? '' : 'h-full'} flex flex-col ${
|
|
||||||
fullscreenEditor === 'headers' ? 'fixed bg-white w-[100vw] h-[100vh] z-[9999] top-0 left-0 p-4' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<label className="block text-sm font-medium">Headers (JSON)</label>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => toggleFullscreen('headers')}
|
|
||||||
>
|
|
||||||
<Maximize className="h-4 w-4 mr-1" />
|
|
||||||
{fullscreenEditor === 'headers' ? '退出全屏' : '全屏'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="fullscreen-headers"
|
|
||||||
className={`${fullscreenEditor === 'headers' ? 'h-full' : 'flex-1'} border border-gray-300 rounded-md overflow-hidden relative`}
|
|
||||||
>
|
|
||||||
<MonacoEditor
|
|
||||||
height="100%"
|
|
||||||
language="json"
|
|
||||||
value={requestData.headers}
|
|
||||||
onChange={(value) => setRequestData(prev => ({ ...prev, headers: value || '{}' }))}
|
|
||||||
onMount={(editor) => {
|
|
||||||
headersEditorRef.current = editor;
|
|
||||||
}}
|
|
||||||
options={{
|
|
||||||
minimap: { enabled: fullscreenEditor === 'headers' },
|
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
fontSize: 14,
|
|
||||||
lineNumbers: 'on',
|
|
||||||
wordWrap: 'on',
|
|
||||||
automaticLayout: true,
|
|
||||||
formatOnPaste: true,
|
|
||||||
formatOnType: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="body" className="flex-1 mt-2">
|
|
||||||
<div
|
|
||||||
className={`${fullscreenEditor === 'body' ? '' : 'h-full'} flex flex-col ${
|
|
||||||
fullscreenEditor === 'body' ? 'fixed bg-white w-[100vw] h-[100vh] z-[9999] top-0 left-0 p-4' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<label className="block text-sm font-medium">Body (JSON)</label>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => toggleFullscreen('body')}
|
|
||||||
>
|
|
||||||
<Maximize className="h-4 w-4 mr-1" />
|
|
||||||
{fullscreenEditor === 'body' ? '退出全屏' : '全屏'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="fullscreen-body"
|
|
||||||
className={`${fullscreenEditor === 'body' ? 'h-full' : 'flex-1'} border border-gray-300 rounded-md overflow-hidden relative`}
|
|
||||||
>
|
|
||||||
<MonacoEditor
|
|
||||||
height="100%"
|
|
||||||
language="json"
|
|
||||||
value={requestData.body}
|
|
||||||
onChange={(value) => setRequestData(prev => ({ ...prev, body: value || '{}' }))}
|
|
||||||
onMount={(editor) => {
|
|
||||||
bodyEditorRef.current = editor;
|
|
||||||
}}
|
|
||||||
options={{
|
|
||||||
minimap: { enabled: fullscreenEditor === 'body' },
|
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
fontSize: 14,
|
|
||||||
lineNumbers: 'on',
|
|
||||||
wordWrap: 'on',
|
|
||||||
automaticLayout: true,
|
|
||||||
formatOnPaste: true,
|
|
||||||
formatOnType: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 下部分:响应信息查看 */}
|
|
||||||
<div className="h-1/2 flex flex-col gap-4">
|
|
||||||
<div className="flex-1 bg-white rounded-lg border p-4 flex flex-col">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="font-medium">响应信息</h3>
|
|
||||||
{responseData.status > 0 && (
|
|
||||||
<div className="flex items-center gap-4 text-sm">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
状态码: <span className={`font-mono px-2 py-1 rounded ${
|
|
||||||
responseData.status >= 200 && responseData.status < 300
|
|
||||||
? 'bg-green-100 text-green-800'
|
|
||||||
: responseData.status >= 400
|
|
||||||
? 'bg-red-100 text-red-800'
|
|
||||||
: 'bg-yellow-100 text-yellow-800'
|
|
||||||
}`}>
|
|
||||||
{responseData.status}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
响应时间: <span className="font-mono">{responseData.responseTime}ms</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{responseData.body ? (
|
|
||||||
<div className="flex-1">
|
|
||||||
<Tabs defaultValue="body" className="h-full flex flex-col">
|
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
|
||||||
<TabsTrigger value="body">响应体</TabsTrigger>
|
|
||||||
<TabsTrigger value="headers">响应头</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="body" className="flex-1 mt-2">
|
|
||||||
<div className="bg-gray-50 border rounded-md p-3 h-full overflow-auto">
|
|
||||||
<pre className="text-sm whitespace-pre-wrap">
|
|
||||||
{responseData.body}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="headers" className="flex-1 mt-2">
|
|
||||||
<div className="bg-gray-50 border rounded-md p-3 h-full overflow-auto">
|
|
||||||
<pre className="text-sm">
|
|
||||||
{responseData.headers}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex-1 flex items-center justify-center text-gray-500">
|
|
||||||
{isLoading ? '发送请求中...' : '发送请求后将在此显示响应'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* 请求历史抽屉 */}
|
|
||||||
<RequestHistoryDrawer
|
|
||||||
isOpen={isHistoryDrawerOpen}
|
|
||||||
onClose={() => setIsHistoryDrawerOpen(false)}
|
|
||||||
onSelectRequest={handleSelectRequest}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { X, RefreshCw, Download, Trash2, ArrowLeft, File, Layers, Bug } from 'lucide-react';
|
import { X, RefreshCw, Download, Trash2, ArrowLeft, File, Layers } from 'lucide-react';
|
||||||
|
|
||||||
interface LogViewerProps {
|
interface LogViewerProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -18,7 +17,6 @@ interface LogEntry {
|
|||||||
message: string; // 现在这个字段直接包含原始JSON字符串
|
message: string; // 现在这个字段直接包含原始JSON字符串
|
||||||
source?: string;
|
source?: string;
|
||||||
reqId?: string;
|
reqId?: string;
|
||||||
[key: string]: any; // 允许动态属性,如msg、url、body等
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LogFile {
|
interface LogFile {
|
||||||
@@ -52,7 +50,6 @@ interface GroupedLogsResponse {
|
|||||||
|
|
||||||
export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
|
||||||
const [logs, setLogs] = useState<string[]>([]);
|
const [logs, setLogs] = useState<string[]>([]);
|
||||||
const [logFiles, setLogFiles] = useState<LogFile[]>([]);
|
const [logFiles, setLogFiles] = useState<LogFile[]>([]);
|
||||||
const [selectedFile, setSelectedFile] = useState<LogFile | null>(null);
|
const [selectedFile, setSelectedFile] = useState<LogFile | null>(null);
|
||||||
@@ -66,7 +63,6 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const refreshInterval = useRef<NodeJS.Timeout | null>(null);
|
const refreshInterval = useRef<NodeJS.Timeout | null>(null);
|
||||||
const workerRef = useRef<Worker | null>(null);
|
const workerRef = useRef<Worker | null>(null);
|
||||||
const editorRef = useRef<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -511,183 +507,6 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
|||||||
return logs.join('\n');
|
return logs.join('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 解析日志行,获取final request的行号
|
|
||||||
const getFinalRequestLines = () => {
|
|
||||||
const lines: number[] = [];
|
|
||||||
|
|
||||||
if (groupByReqId && groupedLogs && selectedReqId && groupedLogs.groups[selectedReqId]) {
|
|
||||||
// 分组模式下,检查选中的请求日志
|
|
||||||
const requestLogs = groupedLogs.groups[selectedReqId];
|
|
||||||
requestLogs.forEach((log, index) => {
|
|
||||||
try {
|
|
||||||
// @ts-ignore
|
|
||||||
log = JSON.parse(log)
|
|
||||||
// 检查日志的msg字段是否等于"final request"
|
|
||||||
if (log.msg === "final request") {
|
|
||||||
lines.push(index + 1); // 行号从1开始
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 解析失败,跳过
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 非分组模式下,检查原始日志
|
|
||||||
logs.forEach((logLine, index) => {
|
|
||||||
try {
|
|
||||||
const log = JSON.parse(logLine);
|
|
||||||
// 检查日志的msg字段是否等于"final request"
|
|
||||||
if (log.msg === "final request") {
|
|
||||||
lines.push(index + 1); // 行号从1开始
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 解析失败,跳过
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理调试按钮点击
|
|
||||||
const handleDebugClick = (lineNumber: number) => {
|
|
||||||
console.log('handleDebugClick called with lineNumber:', lineNumber);
|
|
||||||
console.log('Current state:', { groupByReqId, selectedReqId, logsLength: logs.length });
|
|
||||||
|
|
||||||
let logData = null;
|
|
||||||
|
|
||||||
if (groupByReqId && groupedLogs && selectedReqId && groupedLogs.groups[selectedReqId]) {
|
|
||||||
// 分组模式下获取日志数据
|
|
||||||
const requestLogs = groupedLogs.groups[selectedReqId];
|
|
||||||
console.log('Group mode - requestLogs length:', requestLogs.length);
|
|
||||||
logData = requestLogs[lineNumber - 1]; // 行号转换为数组索引
|
|
||||||
console.log('Group mode - logData:', logData);
|
|
||||||
} else {
|
|
||||||
// 非分组模式下获取日志数据
|
|
||||||
console.log('Non-group mode - logs length:', logs.length);
|
|
||||||
try {
|
|
||||||
const logLine = logs[lineNumber - 1];
|
|
||||||
console.log('Log line:', logLine);
|
|
||||||
logData = JSON.parse(logLine);
|
|
||||||
console.log('Parsed logData:', logData);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse log data:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logData) {
|
|
||||||
console.log('Navigating to debug page with logData:', logData);
|
|
||||||
// 导航到调试页面,并传递日志数据作为URL参数
|
|
||||||
const logDataParam = encodeURIComponent(JSON.stringify(logData));
|
|
||||||
console.log('Encoded logDataParam length:', logDataParam.length);
|
|
||||||
navigate(`/debug?logData=${logDataParam}`);
|
|
||||||
} else {
|
|
||||||
console.error('No log data found for line:', lineNumber);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 配置Monaco Editor
|
|
||||||
const configureEditor = (editor: any) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
// 启用glyph margin
|
|
||||||
editor.updateOptions({
|
|
||||||
glyphMargin: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 存储当前的装饰ID
|
|
||||||
let currentDecorations: string[] = [];
|
|
||||||
|
|
||||||
// 添加glyph margin装饰
|
|
||||||
const updateDecorations = () => {
|
|
||||||
const finalRequestLines = getFinalRequestLines();
|
|
||||||
const decorations = finalRequestLines.map(lineNumber => ({
|
|
||||||
range: {
|
|
||||||
startLineNumber: lineNumber,
|
|
||||||
startColumn: 1,
|
|
||||||
endLineNumber: lineNumber,
|
|
||||||
endColumn: 1
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
glyphMarginClassName: 'debug-button-glyph',
|
|
||||||
glyphMarginHoverMessage: { value: '点击调试此请求' }
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 使用deltaDecorations正确更新装饰,清理旧的装饰
|
|
||||||
currentDecorations = editor.deltaDecorations(currentDecorations, decorations);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始更新装饰
|
|
||||||
updateDecorations();
|
|
||||||
|
|
||||||
// 监听glyph margin点击 - 使用正确的事件监听方式
|
|
||||||
editor.onMouseDown((e: any) => {
|
|
||||||
console.log('Mouse down event:', e.target);
|
|
||||||
console.log('Event details:', {
|
|
||||||
type: e.target.type,
|
|
||||||
hasDetail: !!e.target.detail,
|
|
||||||
glyphMarginLane: e.target.detail?.glyphMarginLane,
|
|
||||||
offsetX: e.target.detail?.offsetX,
|
|
||||||
glyphMarginLeft: e.target.detail?.glyphMarginLeft,
|
|
||||||
glyphMarginWidth: e.target.detail?.glyphMarginWidth
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查是否点击在glyph margin区域
|
|
||||||
const isGlyphMarginClick = e.target.detail &&
|
|
||||||
e.target.detail.glyphMarginLane !== undefined &&
|
|
||||||
e.target.detail.offsetX !== undefined &&
|
|
||||||
e.target.detail.offsetX <= e.target.detail.glyphMarginLeft + e.target.detail.glyphMarginWidth;
|
|
||||||
|
|
||||||
console.log('Is glyph margin click:', isGlyphMarginClick);
|
|
||||||
|
|
||||||
if (e.target.position && isGlyphMarginClick) {
|
|
||||||
const finalRequestLines = getFinalRequestLines();
|
|
||||||
console.log('Final request lines:', finalRequestLines);
|
|
||||||
console.log('Clicked line number:', e.target.position.lineNumber);
|
|
||||||
if (finalRequestLines.includes(e.target.position.lineNumber)) {
|
|
||||||
console.log('Opening debug page for line:', e.target.position.lineNumber);
|
|
||||||
handleDebugClick(e.target.position.lineNumber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 尝试使用 onGlyphMarginClick 如果可用
|
|
||||||
if (typeof editor.onGlyphMarginClick === 'function') {
|
|
||||||
editor.onGlyphMarginClick((e: any) => {
|
|
||||||
console.log('Glyph margin click event:', e);
|
|
||||||
const finalRequestLines = getFinalRequestLines();
|
|
||||||
if (finalRequestLines.includes(e.target.position.lineNumber)) {
|
|
||||||
console.log('Opening debug page for line (glyph):', e.target.position.lineNumber);
|
|
||||||
handleDebugClick(e.target.position.lineNumber);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加鼠标移动事件来检测悬停在调试按钮上
|
|
||||||
editor.onMouseMove((e: any) => {
|
|
||||||
if (e.target.position && (e.target.type === 4 || e.target.type === 'glyph-margin')) {
|
|
||||||
const finalRequestLines = getFinalRequestLines();
|
|
||||||
if (finalRequestLines.includes(e.target.position.lineNumber)) {
|
|
||||||
// 可以在这里添加悬停效果
|
|
||||||
editor.updateOptions({
|
|
||||||
glyphMargin: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 当日志变化时更新装饰
|
|
||||||
const interval = setInterval(updateDecorations, 1000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
// 清理装饰
|
|
||||||
if (editorRef.current) {
|
|
||||||
editorRef.current.deltaDecorations(currentDecorations, []);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isVisible && !open) {
|
if (!isVisible && !open) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -849,27 +668,23 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 显示日志内容
|
// 显示日志内容
|
||||||
<div className="relative h-full">
|
<Editor
|
||||||
<Editor
|
height="100%"
|
||||||
height="100%"
|
defaultLanguage="json"
|
||||||
defaultLanguage="json"
|
value={formatLogsForEditor()}
|
||||||
value={formatLogsForEditor()}
|
theme="vs"
|
||||||
theme="vs"
|
options={{
|
||||||
options={{
|
minimap: { enabled: true },
|
||||||
minimap: { enabled: true },
|
fontSize: 14,
|
||||||
fontSize: 14,
|
scrollBeyondLastLine: false,
|
||||||
scrollBeyondLastLine: false,
|
automaticLayout: true,
|
||||||
automaticLayout: true,
|
wordWrap: 'on',
|
||||||
wordWrap: 'on',
|
readOnly: true,
|
||||||
readOnly: true,
|
lineNumbers: 'on',
|
||||||
lineNumbers: 'on',
|
folding: true,
|
||||||
folding: true,
|
renderWhitespace: 'all',
|
||||||
renderWhitespace: 'all',
|
}}
|
||||||
glyphMargin: true,
|
/>
|
||||||
}}
|
|
||||||
onMount={configureEditor}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { History, Trash2, Clock, X } from 'lucide-react';
|
|
||||||
import { requestHistoryDB, type RequestHistoryItem } from '@/lib/db';
|
|
||||||
|
|
||||||
interface RequestHistoryDrawerProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSelectRequest: (request: RequestHistoryItem) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RequestHistoryDrawer({ isOpen, onClose, onSelectRequest }: RequestHistoryDrawerProps) {
|
|
||||||
const [requests, setRequests] = useState<RequestHistoryItem[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
loadRequests();
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
const loadRequests = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const history = await requestHistoryDB.getRequests();
|
|
||||||
setRequests(history);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load request history:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (id: string, event: React.MouseEvent) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
try {
|
|
||||||
await requestHistoryDB.deleteRequest(id);
|
|
||||||
setRequests(prev => prev.filter(req => req.id !== id));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete request:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearAll = async () => {
|
|
||||||
if (window.confirm('确定要清空所有请求历史吗?')) {
|
|
||||||
try {
|
|
||||||
await requestHistoryDB.clearAllRequests();
|
|
||||||
setRequests([]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to clear request history:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTime = (timestamp: string) => {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
const now = new Date();
|
|
||||||
const diff = now.getTime() - date.getTime();
|
|
||||||
const minutes = Math.floor(diff / 60000);
|
|
||||||
|
|
||||||
if (minutes < 1) return '刚刚';
|
|
||||||
if (minutes < 60) return `${minutes}分钟前`;
|
|
||||||
if (minutes < 1440) return `${Math.floor(minutes / 60)}小时前`;
|
|
||||||
return date.toLocaleDateString();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50">
|
|
||||||
{/* 遮罩层 */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-black bg-opacity-50"
|
|
||||||
onClick={onClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 抽屉 */}
|
|
||||||
<div className="absolute right-0 top-0 h-full w-96 bg-white shadow-xl flex flex-col">
|
|
||||||
{/* 头部 */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<History className="h-5 w-5" />
|
|
||||||
<h2 className="text-lg font-semibold">请求历史</h2>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleClearAll}
|
|
||||||
disabled={requests.length === 0}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 mr-1" />
|
|
||||||
清空
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 内容 */}
|
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center justify-center h-32 text-gray-500">
|
|
||||||
加载中...
|
|
||||||
</div>
|
|
||||||
) : requests.length > 0 ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{requests.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className="p-3 bg-gray-50 rounded-lg border cursor-pointer hover:bg-gray-100 transition-colors"
|
|
||||||
onClick={() => {
|
|
||||||
onSelectRequest(item);
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="font-mono text-xs bg-gray-200 px-2 py-1 rounded">
|
|
||||||
{item.method}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium truncate flex-1">
|
|
||||||
{item.url}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => handleDelete(item.id, e)}
|
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-xs text-gray-500">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className={`font-mono px-1 rounded ${
|
|
||||||
item.status >= 200 && item.status < 300
|
|
||||||
? 'bg-green-100 text-green-800'
|
|
||||||
: item.status >= 400
|
|
||||||
? 'bg-red-100 text-red-800'
|
|
||||||
: 'bg-yellow-100 text-yellow-800'
|
|
||||||
}`}>
|
|
||||||
{item.status}
|
|
||||||
</span>
|
|
||||||
<span>{item.responseTime}ms</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Clock className="h-3 w-3" />
|
|
||||||
<span>{formatTime(item.timestamp)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center text-gray-500 py-8">
|
|
||||||
<History className="h-12 w-12 mx-auto mb-4 text-gray-300" />
|
|
||||||
<p>暂无请求历史</p>
|
|
||||||
<p className="text-sm mt-2">发送请求后会在此显示历史记录</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root
|
|
||||||
|
|
||||||
const TabsList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TabsPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TabsList.displayName = TabsPrimitive.List.displayName
|
|
||||||
|
|
||||||
const TabsTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const TabsContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 美化滚动条 - WebKit浏览器 (Chrome, Safari, Edge) */
|
/* 美化滚动条 - WebKit浏览器 (Chrome, Safari, Edge) */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@@ -153,25 +153,4 @@
|
|||||||
.dark * {
|
.dark * {
|
||||||
scrollbar-color: oklch(0.708 0 0) oklch(0.269 0 0);
|
scrollbar-color: oklch(0.708 0 0) oklch(0.269 0 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Monaco Editor 调试按钮样式 */
|
|
||||||
.debug-button-glyph {
|
|
||||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="%23056bfe" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v-9"/><path d="M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z"/><path d="M14.12 3.88 16 2"/><path d="M21 21a4 4 0 0 0-3.81-4"/><path d="M21 5a4 4 0 0 1-3.55 3.97"/><path d="M22 13h-4"/><path d="M3 21a4 4 0 0 1 3.81-4"/><path d="M3 5a4 4 0 0 0 3.55 3.97"/><path d="M6 13H2"/><path d="m8 2 1.88 1.88"/><path d="M9 7.13V6a3 3 0 1 1 6 0v1.13"/></svg>') center center no-repeat;
|
|
||||||
background-size: 14px 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug-button-glyph:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保调试按钮在glyph margin中可见 */
|
|
||||||
.monaco-editor .margin-view-overlays .debug-button-glyph {
|
|
||||||
display: block !important;
|
|
||||||
width: 16px !important;
|
|
||||||
height: 16px !important;
|
|
||||||
margin: 2px 0;
|
|
||||||
}
|
|
||||||
106
ui/src/lib/db.ts
106
ui/src/lib/db.ts
@@ -1,106 +0,0 @@
|
|||||||
export interface RequestHistoryItem {
|
|
||||||
id: string;
|
|
||||||
url: string;
|
|
||||||
method: string;
|
|
||||||
headers: string;
|
|
||||||
body: string;
|
|
||||||
timestamp: string;
|
|
||||||
status: number;
|
|
||||||
responseTime: number;
|
|
||||||
responseBody: string;
|
|
||||||
responseHeaders: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RequestHistoryDB {
|
|
||||||
private readonly DB_NAME = 'RequestHistoryDB';
|
|
||||||
private readonly STORE_NAME = 'requests';
|
|
||||||
private readonly VERSION = 1;
|
|
||||||
|
|
||||||
async openDB(): Promise<IDBDatabase> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open(this.DB_NAME, this.VERSION);
|
|
||||||
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
|
|
||||||
request.onupgradeneeded = (event) => {
|
|
||||||
const db = (event.target as IDBOpenDBRequest).result;
|
|
||||||
|
|
||||||
if (!db.objectStoreNames.contains(this.STORE_NAME)) {
|
|
||||||
const store = db.createObjectStore(this.STORE_NAME, { keyPath: 'id' });
|
|
||||||
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
||||||
store.createIndex('url', 'url', { unique: false });
|
|
||||||
store.createIndex('method', 'method', { unique: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveRequest(request: Omit<RequestHistoryItem, 'id' | 'timestamp'>): Promise<void> {
|
|
||||||
const db = await this.openDB();
|
|
||||||
const item: RequestHistoryItem = {
|
|
||||||
...request,
|
|
||||||
id: Date.now().toString(),
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = db.transaction([this.STORE_NAME], 'readwrite');
|
|
||||||
const store = transaction.objectStore(this.STORE_NAME);
|
|
||||||
const request = store.add(item);
|
|
||||||
|
|
||||||
request.onsuccess = () => resolve();
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRequests(limit: number = 50): Promise<RequestHistoryItem[]> {
|
|
||||||
const db = await this.openDB();
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = db.transaction([this.STORE_NAME], 'readonly');
|
|
||||||
const store = transaction.objectStore(this.STORE_NAME);
|
|
||||||
const index = store.index('timestamp');
|
|
||||||
const request = index.openCursor(null, 'prev');
|
|
||||||
|
|
||||||
const results: RequestHistoryItem[] = [];
|
|
||||||
|
|
||||||
request.onsuccess = (event) => {
|
|
||||||
const cursor = (event.target as IDBRequest).result;
|
|
||||||
if (cursor && results.length < limit) {
|
|
||||||
results.push(cursor.value);
|
|
||||||
cursor.continue();
|
|
||||||
} else {
|
|
||||||
resolve(results);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteRequest(id: string): Promise<void> {
|
|
||||||
const db = await this.openDB();
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = db.transaction([this.STORE_NAME], 'readwrite');
|
|
||||||
const store = transaction.objectStore(this.STORE_NAME);
|
|
||||||
const request = store.delete(id);
|
|
||||||
|
|
||||||
request.onsuccess = () => resolve();
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearAllRequests(): Promise<void> {
|
|
||||||
const db = await this.openDB();
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = db.transaction([this.STORE_NAME], 'readwrite');
|
|
||||||
const store = transaction.objectStore(this.STORE_NAME);
|
|
||||||
const request = store.clear();
|
|
||||||
|
|
||||||
request.onsuccess = () => resolve();
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const requestHistoryDB = new RequestHistoryDB();
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createMemoryRouter, Navigate } from 'react-router-dom';
|
import { createMemoryRouter, Navigate } from 'react-router-dom';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { Login } from '@/components/Login';
|
import { Login } from '@/components/Login';
|
||||||
import { DebugPage } from '@/components/DebugPage';
|
|
||||||
import ProtectedRoute from '@/components/ProtectedRoute';
|
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||||
import PublicRoute from '@/components/PublicRoute';
|
import PublicRoute from '@/components/PublicRoute';
|
||||||
|
|
||||||
@@ -18,10 +17,6 @@ export const router = createMemoryRouter([
|
|||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
element: <ProtectedRoute><App /></ProtectedRoute>,
|
element: <ProtectedRoute><App /></ProtectedRoute>,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/debug',
|
|
||||||
element: <ProtectedRoute><DebugPage /></ProtectedRoute>,
|
|
||||||
},
|
|
||||||
], {
|
], {
|
||||||
initialEntries: ['/dashboard']
|
initialEntries: ['/dashboard']
|
||||||
});
|
});
|
||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/debugpage.tsx","./src/components/jsoneditor.tsx","./src/components/logviewer.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/requesthistorydrawer.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/statuslineconfigdialog.tsx","./src/components/statuslineimportexport.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/db.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"}
|
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/logviewer.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/statuslineconfigdialog.tsx","./src/components/statuslineimportexport.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"}
|
||||||
Reference in New Issue
Block a user