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)
|
||||
|
||||
|
||||
[中文版](README_zh.md)
|
||||
|
||||
> 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).
|
||||
@@ -13,6 +16,8 @@ I am seeking funding support for this project to better sustain its development.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **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
|
||||
|
||||
- @\*政
|
||||
- @\*铭
|
||||
- @\*叶
|
||||
|
||||
(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)
|
||||
|
||||
@@ -10,6 +16,9 @@
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
## ✨ 功能
|
||||
|
||||
- **模型路由**: 根据您的需求将请求路由到不同的模型(例如,后台任务、思考、长上下文)。
|
||||
@@ -541,6 +550,9 @@ jobs:
|
||||
- @\*更
|
||||
- @\*.
|
||||
- @F\*t
|
||||
- @\*政
|
||||
- @\*铭
|
||||
- @\*叶
|
||||
|
||||
(如果您的名字被屏蔽,请通过我的主页电子邮件与我联系,以便使用您的 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",
|
||||
"version": "1.0.47",
|
||||
"version": "1.0.49",
|
||||
"description": "Use Claude Code without an Anthropics account and route it to another LLM provider",
|
||||
"bin": {
|
||||
"ccr": "./dist/cli.js"
|
||||
@@ -22,6 +22,7 @@
|
||||
"@fastify/static": "^8.2.0",
|
||||
"@musistudio/llms": "^1.0.32",
|
||||
"dotenv": "^16.4.7",
|
||||
"find-process": "^2.0.0",
|
||||
"json5": "^2.2.3",
|
||||
"openurl": "^1.1.1",
|
||||
"rotating-file-stream": "^3.2.7",
|
||||
|
||||
48
pnpm-lock.yaml
generated
48
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.6.1
|
||||
find-process:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
json5:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
@@ -338,6 +341,10 @@ packages:
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@@ -345,6 +352,10 @@ packages:
|
||||
color-name@1.1.4:
|
||||
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:
|
||||
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -460,6 +471,10 @@ packages:
|
||||
resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
find-process@2.0.0:
|
||||
resolution: {integrity: sha512-YUBQnteWGASJoEVVsOXy6XtKAY2O1FCsWnnvQ8y0YwgY1rZiKeVptnFvMu6RSELZAJOGklqseTnUGGs5D0bKmg==}
|
||||
hasBin: true
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -524,6 +539,10 @@ packages:
|
||||
resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -609,6 +628,10 @@ packages:
|
||||
light-my-request@6.6.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -865,6 +888,10 @@ packages:
|
||||
resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==}
|
||||
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:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1188,12 +1215,19 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
commander@12.1.0: {}
|
||||
|
||||
content-disposition@0.5.4:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
@@ -1351,6 +1385,12 @@ snapshots:
|
||||
fast-querystring: 1.1.2
|
||||
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:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
@@ -1458,6 +1498,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
@@ -1538,6 +1580,8 @@ snapshots:
|
||||
process-warning: 4.0.1
|
||||
set-cookie-parser: 2.7.1
|
||||
|
||||
loglevel@1.9.2: {}
|
||||
|
||||
lru-cache@11.1.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
@@ -1741,6 +1785,10 @@ snapshots:
|
||||
|
||||
strip-eof@1.0.0: {}
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
thread-stream@3.1.0:
|
||||
|
||||
@@ -45,7 +45,8 @@ async function waitForService(
|
||||
|
||||
const startTime = Date.now();
|
||||
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
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
return true;
|
||||
@@ -56,6 +57,7 @@ async function waitForService(
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const isRunning = await isServiceRunning()
|
||||
switch (command) {
|
||||
case "start":
|
||||
run();
|
||||
@@ -108,7 +110,7 @@ async function main() {
|
||||
});
|
||||
break;
|
||||
case "code":
|
||||
if (!isServiceRunning()) {
|
||||
if (!isRunning) {
|
||||
console.log("Service not running, starting service...");
|
||||
const cliPath = join(__dirname, "cli.js");
|
||||
const startProcess = spawn("node", [cliPath, "start"], {
|
||||
@@ -153,7 +155,7 @@ async function main() {
|
||||
break;
|
||||
case "ui":
|
||||
// Check if service is running
|
||||
if (!isServiceRunning()) {
|
||||
if (!isRunning) {
|
||||
console.log("Service not running, starting service...");
|
||||
const cliPath = join(__dirname, "cli.js");
|
||||
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 = {}) {
|
||||
// Check if service is already running
|
||||
if (isServiceRunning()) {
|
||||
const isRunning = await isServiceRunning()
|
||||
if (isRunning) {
|
||||
console.log("✅ Service is already running in the background.");
|
||||
return;
|
||||
}
|
||||
@@ -244,7 +245,6 @@ async function run(options: RunOptions = {}) {
|
||||
req,
|
||||
config
|
||||
});
|
||||
console.log('result', toolResult)
|
||||
toolMessages.push({
|
||||
"tool_use_id": currentToolId,
|
||||
"type": "tool_result",
|
||||
@@ -295,14 +295,12 @@ async function run(options: RunOptions = {}) {
|
||||
|
||||
// 检查流是否仍然可写
|
||||
if (!controller.desiredSize) {
|
||||
console.log('Stream backpressure detected');
|
||||
break;
|
||||
}
|
||||
|
||||
controller.enqueue(value)
|
||||
}catch (readError: any) {
|
||||
if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||
console.log('Stream reading aborted due to client disconnect');
|
||||
abortController.abort(); // 中止所有相关操作
|
||||
break;
|
||||
}
|
||||
@@ -318,7 +316,6 @@ async function run(options: RunOptions = {}) {
|
||||
|
||||
// 处理流提前关闭的错误
|
||||
if (error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||
console.log('Stream prematurely closed, aborting operations');
|
||||
abortController.abort();
|
||||
return undefined;
|
||||
}
|
||||
@@ -349,7 +346,7 @@ async function run(options: RunOptions = {}) {
|
||||
}
|
||||
} catch (readError: any) {
|
||||
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 {
|
||||
console.error('Error in background stream reading:', readError);
|
||||
}
|
||||
@@ -361,6 +358,13 @@ async function run(options: RunOptions = {}) {
|
||||
return done(null, originalStream)
|
||||
}
|
||||
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) {
|
||||
return done(payload.error, null)
|
||||
@@ -368,7 +372,6 @@ async function run(options: RunOptions = {}) {
|
||||
done(null, payload)
|
||||
});
|
||||
server.addHook("onSend", async (req, reply, payload) => {
|
||||
console.log('主应用onSend')
|
||||
event.emit('onSend', req, reply, payload);
|
||||
return payload;
|
||||
})
|
||||
|
||||
@@ -5,8 +5,9 @@ import { join } from 'path';
|
||||
|
||||
export async function closeService() {
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { PID_FILE, REFERENCE_COUNT_FILE } from '../constants';
|
||||
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() {
|
||||
let count = 0;
|
||||
@@ -27,15 +37,14 @@ export function getReferenceCount(): number {
|
||||
return parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0;
|
||||
}
|
||||
|
||||
export function isServiceRunning(): boolean {
|
||||
export async function isServiceRunning(): Promise<boolean> {
|
||||
if (!existsSync(PID_FILE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const pid = parseInt(readFileSync(PID_FILE, 'utf-8'));
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
return await isProcessRunning(pid);
|
||||
} catch (e) {
|
||||
// Process not running, clean up pid file
|
||||
cleanupPidFile();
|
||||
@@ -73,7 +82,7 @@ export function getServicePid(): number | null {
|
||||
|
||||
export async function getServiceInfo() {
|
||||
const pid = getServicePid();
|
||||
const running = isServiceRunning();
|
||||
const running = await isServiceRunning();
|
||||
const config = await readConfigFile();
|
||||
const port = config.PORT || 3456;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from "@anthropic-ai/sdk/resources/messages";
|
||||
import { get_encoding } from "tiktoken";
|
||||
import { sessionUsageCache, Usage } from "./cache";
|
||||
import { readFile } from 'fs/promises'
|
||||
|
||||
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 { 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 {
|
||||
const tokenCount = calculateTokenCount(
|
||||
messages as MessageParam[],
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"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':
|
||||
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)
|
||||
'@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':
|
||||
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)
|
||||
@@ -520,19 +517,6 @@ packages:
|
||||
'@types/react-dom':
|
||||
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':
|
||||
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
||||
peerDependencies:
|
||||
@@ -564,15 +548,6 @@ packages:
|
||||
'@types/react-dom':
|
||||
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':
|
||||
resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==}
|
||||
peerDependencies:
|
||||
@@ -734,19 +709,6 @@ packages:
|
||||
'@types/react-dom':
|
||||
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':
|
||||
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
||||
peerDependencies:
|
||||
@@ -769,19 +731,6 @@ packages:
|
||||
'@types/react-dom':
|
||||
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':
|
||||
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
||||
peerDependencies:
|
||||
@@ -2384,18 +2333,6 @@ snapshots:
|
||||
'@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)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
@@ -2430,12 +2367,6 @@ snapshots:
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.2
|
||||
@@ -2593,23 +2524,6 @@ snapshots:
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@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-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)':
|
||||
dependencies:
|
||||
'@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 { useNavigate } from 'react-router-dom';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { api } from '@/lib/api';
|
||||
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 {
|
||||
open: boolean;
|
||||
@@ -18,7 +17,6 @@ interface LogEntry {
|
||||
message: string; // 现在这个字段直接包含原始JSON字符串
|
||||
source?: string;
|
||||
reqId?: string;
|
||||
[key: string]: any; // 允许动态属性,如msg、url、body等
|
||||
}
|
||||
|
||||
interface LogFile {
|
||||
@@ -52,7 +50,6 @@ interface GroupedLogsResponse {
|
||||
|
||||
export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [logFiles, setLogFiles] = useState<LogFile[]>([]);
|
||||
const [selectedFile, setSelectedFile] = useState<LogFile | null>(null);
|
||||
@@ -66,7 +63,6 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const refreshInterval = useRef<NodeJS.Timeout | null>(null);
|
||||
const workerRef = useRef<Worker | null>(null);
|
||||
const editorRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -511,183 +507,6 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
@@ -849,27 +668,23 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
||||
</div>
|
||||
) : (
|
||||
// 显示日志内容
|
||||
<div className="relative h-full">
|
||||
<Editor
|
||||
height="100%"
|
||||
defaultLanguage="json"
|
||||
value={formatLogsForEditor()}
|
||||
theme="vs"
|
||||
options={{
|
||||
minimap: { enabled: true },
|
||||
fontSize: 14,
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
wordWrap: 'on',
|
||||
readOnly: true,
|
||||
lineNumbers: 'on',
|
||||
folding: true,
|
||||
renderWhitespace: 'all',
|
||||
glyphMargin: true,
|
||||
}}
|
||||
onMount={configureEditor}
|
||||
/>
|
||||
</div>
|
||||
<Editor
|
||||
height="100%"
|
||||
defaultLanguage="json"
|
||||
value={formatLogsForEditor()}
|
||||
theme="vs"
|
||||
options={{
|
||||
minimap: { enabled: true },
|
||||
fontSize: 14,
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
wordWrap: 'on',
|
||||
readOnly: true,
|
||||
lineNumbers: 'on',
|
||||
folding: true,
|
||||
renderWhitespace: 'all',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -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 }
|
||||
@@ -154,24 +154,3 @@
|
||||
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 App from './App';
|
||||
import { Login } from '@/components/Login';
|
||||
import { DebugPage } from '@/components/DebugPage';
|
||||
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||
import PublicRoute from '@/components/PublicRoute';
|
||||
|
||||
@@ -18,10 +17,6 @@ export const router = createMemoryRouter([
|
||||
path: '/dashboard',
|
||||
element: <ProtectedRoute><App /></ProtectedRoute>,
|
||||
},
|
||||
{
|
||||
path: '/debug',
|
||||
element: <ProtectedRoute><DebugPage /></ProtectedRoute>,
|
||||
},
|
||||
], {
|
||||
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