4 Commits

Author SHA1 Message Date
musi
5cd21c570f Merge pull request #798 from SaseQ/main
Add ccr logo and badges (README.md edit)
2025-09-10 20:39:37 +08:00
SaseQ
c5e97709a5 Add ccr logo and badges (README.md edit) 2025-09-10 14:32:02 +02:00
musistudio
f7adb7b28e release v1.0.49 2025-09-09 22:43:01 +08:00
musistudio
7964fff175 release v1.0.48 2025-09-09 21:47:59 +08:00
21 changed files with 203 additions and 1185 deletions

View File

@@ -1,10 +1,13 @@
# Claude Code Router ![](blog/images/claude-code-router-img.png)
[![](https://img.shields.io/badge/%F0%9F%87%A8%F0%9F%87%B3-%E4%B8%AD%E6%96%87%E7%89%88-ff0000?style=flat)](README_zh.md)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white)](https://discord.gg/rdftVMaUcS)
[![](https://img.shields.io/github/license/musistudio/claude-code-router)](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.
![](blog/images/claude-code.png) ![](blog/images/claude-code.png)
![](blog/images/roadmap.svg)
## ✨ 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.)

View File

@@ -1,4 +1,10 @@
# Claude Code Router ![](blog/images/claude-code-router-img.png)
[![](https://img.shields.io/badge/%F0%9F%87%AC%F0%9F%87%A7-English-000aff?style=flat)](README.md)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white)](https://discord.gg/rdftVMaUcS)
[![](https://img.shields.io/github/license/musistudio/claude-code-router)](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 @@
![](blog/images/claude-code.png) ![](blog/images/claude-code.png)
![](blog/images/roadmap.svg)
## ✨ 功能 ## ✨ 功能
- **模型路由**: 根据您的需求将请求路由到不同的模型(例如,后台任务、思考、长上下文)。 - **模型路由**: 根据您的需求将请求路由到不同的模型(例如,后台任务、思考、长上下文)。
@@ -541,6 +550,9 @@ jobs:
- @\*更 - @\*更
- @\*. - @\*.
- @F\*t - @F\*t
- @\*政
- @\*铭
- @\*叶
(如果您的名字被屏蔽,请通过我的主页电子邮件与我联系,以便使用您的 GitHub 用户名进行更新。) (如果您的名字被屏蔽,请通过我的主页电子邮件与我联系,以便使用您的 GitHub 用户名进行更新。)

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

67
blog/images/roadmap.svg Normal file
View 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 &amp; 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 &amp; 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

View File

@@ -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
View File

@@ -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:

View File

@@ -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"], {

View File

@@ -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;
}) })

View File

@@ -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;
} }

View File

@@ -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,

View File

@@ -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[],

View File

@@ -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
View File

@@ -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

View File

@@ -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>
);
}

View File

@@ -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>
)} )}
</> </>
) : ( ) : (

View File

@@ -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>
);
}

View File

@@ -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 }

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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']
}); });

View File

@@ -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"}