ai-agent-router 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/openspec/apply.md +23 -0
- package/.claude/commands/openspec/archive.md +27 -0
- package/.claude/commands/openspec/proposal.md +28 -0
- package/.claude/settings.local.json +12 -0
- package/.claude/skills/ui-ux-pro-max/SKILL.md +228 -0
- package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/prompts.csv +24 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.claude/skills/ui-ux-pro-max/data/styles.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc +0 -0
- package/.claude/skills/ui-ux-pro-max/scripts/core.py +238 -0
- package/.claude/skills/ui-ux-pro-max/scripts/search.py +61 -0
- package/.cursor/commands/openspec-apply.md +23 -0
- package/.cursor/commands/openspec-archive.md +27 -0
- package/.cursor/commands/openspec-proposal.md +28 -0
- package/.cursor/commands/ui-ux-pro-max.md +226 -0
- package/.eslintrc.json +3 -0
- package/.shared/ui-ux-pro-max/data/charts.csv +26 -0
- package/.shared/ui-ux-pro-max/data/colors.csv +97 -0
- package/.shared/ui-ux-pro-max/data/landing.csv +31 -0
- package/.shared/ui-ux-pro-max/data/products.csv +97 -0
- package/.shared/ui-ux-pro-max/data/prompts.csv +24 -0
- package/.shared/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.shared/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.shared/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.shared/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.shared/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.shared/ui-ux-pro-max/data/styles.csv +59 -0
- package/.shared/ui-ux-pro-max/data/typography.csv +58 -0
- package/.shared/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.shared/ui-ux-pro-max/scripts/core.py +238 -0
- package/.shared/ui-ux-pro-max/scripts/search.py +61 -0
- package/AGENTS.md +18 -0
- package/CLAUDE.md +18 -0
- package/IMPLEMENTATION.md +157 -0
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/.next/types/app/api/config/route.js +52 -0
- package/dist/.next/types/app/api/gateway/[...path]/route.js +52 -0
- package/dist/.next/types/app/api/gateway/route.js +52 -0
- package/dist/.next/types/app/api/logs/route.js +52 -0
- package/dist/.next/types/app/api/models/route.js +52 -0
- package/dist/.next/types/app/api/providers/route.js +52 -0
- package/dist/.next/types/app/api/providers/test/route.js +52 -0
- package/dist/.next/types/app/api/service/start/route.js +52 -0
- package/dist/.next/types/app/api/service/status/route.js +52 -0
- package/dist/.next/types/app/api/service/stop/route.js +52 -0
- package/dist/.next/types/app/layout.js +22 -0
- package/dist/.next/types/app/logs/page.js +22 -0
- package/dist/.next/types/app/models/page.js +22 -0
- package/dist/.next/types/app/page.js +22 -0
- package/dist/.next/types/app/providers/page.js +22 -0
- package/dist/src/app/api/config/route.js +43 -0
- package/dist/src/app/api/gateway/[...path]/route.js +83 -0
- package/dist/src/app/api/gateway/route.js +63 -0
- package/dist/src/app/api/logs/route.js +34 -0
- package/dist/src/app/api/models/route.js +152 -0
- package/dist/src/app/api/providers/route.js +118 -0
- package/dist/src/app/api/providers/test/route.js +154 -0
- package/dist/src/app/api/service/start/route.js +55 -0
- package/dist/src/app/api/service/status/route.js +17 -0
- package/dist/src/app/api/service/stop/route.js +20 -0
- package/dist/src/app/components/ConfirmDialog.jsx +31 -0
- package/dist/src/app/components/Nav.jsx +45 -0
- package/dist/src/app/components/Toast.jsx +37 -0
- package/dist/src/app/components/ToastProvider.jsx +21 -0
- package/dist/src/app/layout.jsx +13 -0
- package/dist/src/app/logs/page.jsx +210 -0
- package/dist/src/app/models/page.jsx +291 -0
- package/dist/src/app/page.jsx +236 -0
- package/dist/src/app/providers/page.jsx +402 -0
- package/dist/src/cli/index.js +90 -0
- package/dist/src/db/database.js +69 -0
- package/dist/src/db/queries.js +261 -0
- package/dist/src/db/schema.js +67 -0
- package/dist/src/server/crypto.js +22 -0
- package/dist/src/server/gateway-server.js +200 -0
- package/dist/src/server/gateway.js +76 -0
- package/dist/src/server/logger.js +72 -0
- package/dist/src/server/providers/anthropic.js +52 -0
- package/dist/src/server/providers/gemini.js +64 -0
- package/dist/src/server/providers/index.js +16 -0
- package/dist/src/server/providers/openai.js +86 -0
- package/dist/src/server/providers/types.js +1 -0
- package/dist/src/server/service-manager.js +286 -0
- package/docs/TODO.md +19 -0
- package/next.config.js +7 -0
- package/openspec/AGENTS.md +456 -0
- package/openspec/changes/add-logging/proposal.md +18 -0
- package/openspec/changes/add-logging/specs/core/spec.md +21 -0
- package/openspec/changes/add-logging/tasks.md +16 -0
- package/openspec/changes/add-provider-test-connection/proposal.md +22 -0
- package/openspec/changes/add-provider-test-connection/specs/model-provider/spec.md +68 -0
- package/openspec/changes/add-provider-test-connection/tasks.md +31 -0
- package/openspec/changes/improve-gateway-startup/design.md +137 -0
- package/openspec/changes/improve-gateway-startup/proposal.md +33 -0
- package/openspec/changes/improve-gateway-startup/specs/api-gateway/spec.md +94 -0
- package/openspec/changes/improve-gateway-startup/specs/web-ui/spec.md +67 -0
- package/openspec/changes/improve-gateway-startup/tasks.md +47 -0
- package/openspec/changes/init-api-gateway/design.md +185 -0
- package/openspec/changes/init-api-gateway/proposal.md +30 -0
- package/openspec/changes/init-api-gateway/specs/api-gateway/spec.md +42 -0
- package/openspec/changes/init-api-gateway/specs/cli-tool/spec.md +40 -0
- package/openspec/changes/init-api-gateway/specs/model-management/spec.md +47 -0
- package/openspec/changes/init-api-gateway/specs/model-provider/spec.md +33 -0
- package/openspec/changes/init-api-gateway/specs/request-logging/spec.md +54 -0
- package/openspec/changes/init-api-gateway/specs/web-ui/spec.md +49 -0
- package/openspec/changes/init-api-gateway/tasks.md +84 -0
- package/openspec/project.md +58 -0
- package/package.json +51 -0
- package/postcss.config.js +6 -0
- package/src/app/api/config/route.ts +62 -0
- package/src/app/api/gateway/[...path]/route.ts +118 -0
- package/src/app/api/gateway/route.ts +77 -0
- package/src/app/api/logs/route.ts +48 -0
- package/src/app/api/models/route.ts +210 -0
- package/src/app/api/providers/route.ts +162 -0
- package/src/app/api/providers/test/route.ts +182 -0
- package/src/app/api/service/start/route.ts +73 -0
- package/src/app/api/service/status/route.ts +22 -0
- package/src/app/api/service/stop/route.ts +27 -0
- package/src/app/components/ConfirmDialog.tsx +63 -0
- package/src/app/components/Nav.tsx +66 -0
- package/src/app/components/Toast.tsx +61 -0
- package/src/app/components/ToastProvider.tsx +43 -0
- package/src/app/globals.css +71 -0
- package/src/app/layout.tsx +22 -0
- package/src/app/logs/page.tsx +261 -0
- package/src/app/models/page.tsx +500 -0
- package/src/app/page.tsx +742 -0
- package/src/app/providers/page.tsx +558 -0
- package/src/cli/index.ts +95 -0
- package/src/db/database.ts +125 -0
- package/src/db/queries.ts +339 -0
- package/src/db/schema.ts +117 -0
- package/src/server/crypto.ts +48 -0
- package/src/server/gateway-server.ts +306 -0
- package/src/server/gateway.ts +163 -0
- package/src/server/logger.ts +96 -0
- package/src/server/providers/anthropic.ts +121 -0
- package/src/server/providers/gemini.ts +112 -0
- package/src/server/providers/index.ts +20 -0
- package/src/server/providers/openai.ts +235 -0
- package/src/server/providers/types.ts +20 -0
- package/src/server/service-manager.ts +321 -0
- package/tailwind.config.js +16 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getDatabase } from '@/db/database';
|
|
3
|
+
import { serviceManager } from '@/server/service-manager';
|
|
4
|
+
|
|
5
|
+
// Ensure Node.js runtime (required for service manager)
|
|
6
|
+
export const runtime = 'nodejs';
|
|
7
|
+
|
|
8
|
+
export async function POST(request: NextRequest) {
|
|
9
|
+
try {
|
|
10
|
+
// Initialize database
|
|
11
|
+
getDatabase();
|
|
12
|
+
|
|
13
|
+
const result = await serviceManager.stop();
|
|
14
|
+
|
|
15
|
+
if (result.error) {
|
|
16
|
+
return NextResponse.json(result, { status: 400 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return NextResponse.json(result);
|
|
20
|
+
} catch (error: any) {
|
|
21
|
+
console.error('Service stop API error:', error);
|
|
22
|
+
return NextResponse.json(
|
|
23
|
+
{ status: 'stopped', error: error.message || 'Failed to stop service' },
|
|
24
|
+
{ status: 500 }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface ConfirmDialogProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
title: string;
|
|
6
|
+
message: string;
|
|
7
|
+
onConfirm: () => void;
|
|
8
|
+
onCancel: () => void;
|
|
9
|
+
confirmText?: string;
|
|
10
|
+
cancelText?: string;
|
|
11
|
+
type?: 'danger' | 'warning' | 'info';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function ConfirmDialog({
|
|
15
|
+
open,
|
|
16
|
+
title,
|
|
17
|
+
message,
|
|
18
|
+
onConfirm,
|
|
19
|
+
onCancel,
|
|
20
|
+
confirmText = '确认',
|
|
21
|
+
cancelText = '取消',
|
|
22
|
+
type = 'info',
|
|
23
|
+
}: ConfirmDialogProps) {
|
|
24
|
+
if (!open) return null;
|
|
25
|
+
|
|
26
|
+
const buttonColors = {
|
|
27
|
+
danger: 'bg-rose-500 hover:bg-rose-600 focus:ring-rose-500/50',
|
|
28
|
+
warning: 'bg-amber-500 hover:bg-amber-600 focus:ring-amber-500/50',
|
|
29
|
+
info: 'bg-emerald-600 hover:bg-emerald-700 focus:ring-emerald-500/50',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="fixed z-50 inset-0 overflow-y-auto">
|
|
34
|
+
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
35
|
+
<div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm transition-opacity" onClick={onCancel}></div>
|
|
36
|
+
<div className="inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full border border-emerald-100/50">
|
|
37
|
+
<div className="bg-white px-5 pt-5 pb-4 sm:p-6">
|
|
38
|
+
<div className="mb-4">
|
|
39
|
+
<h3 className="text-base font-bold text-slate-800 mb-1">{title}</h3>
|
|
40
|
+
<p className="text-xs text-slate-600">{message}</p>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="flex items-center justify-end space-x-2">
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
onClick={onCancel}
|
|
46
|
+
className="px-3.5 py-1.5 border border-slate-200 rounded-lg text-xs font-semibold text-slate-600 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-slate-400/30 transition-all duration-300"
|
|
47
|
+
>
|
|
48
|
+
{cancelText}
|
|
49
|
+
</button>
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
onClick={onConfirm}
|
|
53
|
+
className={`px-4 py-1.5 border border-transparent rounded-lg text-xs font-semibold text-white focus:outline-none focus:ring-2 focus:ring-offset-1 transition-all duration-300 ${buttonColors[type]}`}
|
|
54
|
+
>
|
|
55
|
+
{confirmText}
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
|
|
6
|
+
export default function Nav() {
|
|
7
|
+
const pathname = usePathname();
|
|
8
|
+
|
|
9
|
+
const navItems = [
|
|
10
|
+
{ href: '/', label: '配置' },
|
|
11
|
+
{ href: '/providers', label: '供应商' },
|
|
12
|
+
{ href: '/models', label: '模型' },
|
|
13
|
+
{ href: '/logs', label: '日志' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<nav className="bg-white/70 backdrop-blur-xl border-b border-emerald-100/30 sticky top-0 z-50">
|
|
18
|
+
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-10">
|
|
19
|
+
<div className="flex items-center justify-between h-16">
|
|
20
|
+
<div className="flex items-center space-x-12">
|
|
21
|
+
<Link href="/" className="flex flex-col group transition-all duration-300">
|
|
22
|
+
<span className="text-xl font-bold text-emerald-700 group-hover:text-emerald-600 transition-colors duration-300 leading-none">
|
|
23
|
+
AAR
|
|
24
|
+
</span>
|
|
25
|
+
<span className="text-[9px] font-medium text-slate-400 tracking-wider uppercase mt-0.5 leading-none">
|
|
26
|
+
AI Agent Router
|
|
27
|
+
</span>
|
|
28
|
+
</Link>
|
|
29
|
+
<div className="hidden sm:flex sm:items-center">
|
|
30
|
+
{navItems.map((item) => {
|
|
31
|
+
const isActive = pathname === item.href;
|
|
32
|
+
return (
|
|
33
|
+
<Link
|
|
34
|
+
key={item.href}
|
|
35
|
+
href={item.href}
|
|
36
|
+
className="relative inline-flex items-center justify-center w-[68px] h-9 text-xs font-medium transition-colors duration-300 group"
|
|
37
|
+
style={{ minWidth: '68px', maxWidth: '68px' }}
|
|
38
|
+
>
|
|
39
|
+
{/* 固定宽度和高度,完全避免抖动 - 使用绝对定位确保位置不变 */}
|
|
40
|
+
<span
|
|
41
|
+
className={`absolute inset-0 flex items-center justify-center transition-colors duration-300 ${
|
|
42
|
+
isActive
|
|
43
|
+
? 'text-emerald-700 font-semibold'
|
|
44
|
+
: 'text-slate-500 group-hover:text-slate-700'
|
|
45
|
+
}`}
|
|
46
|
+
>
|
|
47
|
+
{item.label}
|
|
48
|
+
</span>
|
|
49
|
+
{/* 选中状态背景 - 固定尺寸,不改变布局 */}
|
|
50
|
+
<span
|
|
51
|
+
className={`absolute inset-0 rounded-lg transition-all duration-300 ${
|
|
52
|
+
isActive
|
|
53
|
+
? 'bg-emerald-50/60'
|
|
54
|
+
: 'bg-transparent group-hover:bg-slate-50/40'
|
|
55
|
+
}`}
|
|
56
|
+
/>
|
|
57
|
+
</Link>
|
|
58
|
+
);
|
|
59
|
+
})}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</nav>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
interface ToastProps {
|
|
6
|
+
message: string;
|
|
7
|
+
type?: 'success' | 'error' | 'info';
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
duration?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function Toast({ message, type = 'info', onClose, duration = 3000 }: ToastProps) {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const timer = setTimeout(() => {
|
|
15
|
+
onClose();
|
|
16
|
+
}, duration);
|
|
17
|
+
|
|
18
|
+
return () => clearTimeout(timer);
|
|
19
|
+
}, [duration, onClose]);
|
|
20
|
+
|
|
21
|
+
const colors = {
|
|
22
|
+
success: 'bg-emerald-500 text-white border-emerald-600/30 shadow-emerald-500/20',
|
|
23
|
+
error: 'bg-rose-500 text-white border-rose-600/30 shadow-rose-500/20',
|
|
24
|
+
info: 'bg-slate-600 text-white border-slate-700/30 shadow-slate-600/20',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const icons = {
|
|
28
|
+
success: (
|
|
29
|
+
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
30
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
|
31
|
+
</svg>
|
|
32
|
+
),
|
|
33
|
+
error: (
|
|
34
|
+
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
35
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
|
|
36
|
+
</svg>
|
|
37
|
+
),
|
|
38
|
+
info: (
|
|
39
|
+
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
40
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
41
|
+
</svg>
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="fixed top-20 left-1/2 -translate-x-1/2 z-50 animate-in slide-in-from-top fade-in duration-300">
|
|
47
|
+
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg shadow-lg border backdrop-blur-sm ${colors[type]}`}>
|
|
48
|
+
<span className="flex-shrink-0">{icons[type]}</span>
|
|
49
|
+
<span className="text-xs font-medium leading-tight">{message}</span>
|
|
50
|
+
<button
|
|
51
|
+
onClick={onClose}
|
|
52
|
+
className="ml-1.5 flex-shrink-0 opacity-70 hover:opacity-100 transition-opacity p-0.5"
|
|
53
|
+
>
|
|
54
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
55
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
|
|
56
|
+
</svg>
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, ReactNode } from 'react';
|
|
4
|
+
import Toast from './Toast';
|
|
5
|
+
|
|
6
|
+
interface ToastContextType {
|
|
7
|
+
showToast: (message: string, type?: 'success' | 'error' | 'info') => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
|
11
|
+
|
|
12
|
+
export function useToast() {
|
|
13
|
+
const context = useContext(ToastContext);
|
|
14
|
+
if (!context) {
|
|
15
|
+
throw new Error('useToast must be used within ToastProvider');
|
|
16
|
+
}
|
|
17
|
+
return context;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ToastProviderProps {
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ToastProvider({ children }: ToastProviderProps) {
|
|
25
|
+
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'info' } | null>(null);
|
|
26
|
+
|
|
27
|
+
const showToast = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
|
28
|
+
setToast({ message, type });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<ToastContext.Provider value={{ showToast }}>
|
|
33
|
+
{children}
|
|
34
|
+
{toast && (
|
|
35
|
+
<Toast
|
|
36
|
+
message={toast.message}
|
|
37
|
+
type={toast.type}
|
|
38
|
+
onClose={() => setToast(null)}
|
|
39
|
+
/>
|
|
40
|
+
)}
|
|
41
|
+
</ToastContext.Provider>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
--background: #fafbf9;
|
|
7
|
+
--foreground: #2d3436;
|
|
8
|
+
--emerald-50: #ecfdf5;
|
|
9
|
+
--emerald-100: #d1fae5;
|
|
10
|
+
--emerald-500: #10b981;
|
|
11
|
+
--emerald-700: #047857;
|
|
12
|
+
--slate-50: #f8fafc;
|
|
13
|
+
--slate-100: #f1f5f9;
|
|
14
|
+
--slate-400: #94a3b8;
|
|
15
|
+
--slate-500: #64748b;
|
|
16
|
+
--slate-600: #475569;
|
|
17
|
+
--slate-700: #334155;
|
|
18
|
+
--slate-800: #1e293b;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
body {
|
|
22
|
+
color: var(--foreground);
|
|
23
|
+
background: var(--background);
|
|
24
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
|
25
|
+
-webkit-font-smoothing: antialiased;
|
|
26
|
+
-moz-osx-font-smoothing: grayscale;
|
|
27
|
+
letter-spacing: -0.01em;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@layer utilities {
|
|
31
|
+
.text-balance {
|
|
32
|
+
text-wrap: balance;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* 优化输入框样式 - 使用植物绿和莫兰迪色 */
|
|
37
|
+
input, select, textarea {
|
|
38
|
+
@apply transition-all duration-300;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
input:focus, select:focus, textarea:focus {
|
|
42
|
+
@apply outline-none ring-2 ring-emerald-400/20 ring-offset-2 ring-offset-white;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* 优化按钮样式 */
|
|
46
|
+
button {
|
|
47
|
+
@apply transition-all duration-300;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
button:disabled {
|
|
51
|
+
@apply cursor-not-allowed opacity-50;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* 轻盈的滚动条 */
|
|
55
|
+
::-webkit-scrollbar {
|
|
56
|
+
width: 8px;
|
|
57
|
+
height: 8px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
::-webkit-scrollbar-track {
|
|
61
|
+
background: #f1f5f9;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
::-webkit-scrollbar-thumb {
|
|
65
|
+
background: #cbd5e1;
|
|
66
|
+
border-radius: 4px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
::-webkit-scrollbar-thumb:hover {
|
|
70
|
+
background: #94a3b8;
|
|
71
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import './globals.css';
|
|
3
|
+
import { ToastProvider } from './components/ToastProvider';
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: 'AAR - AI Agent Router',
|
|
7
|
+
description: 'Unified API gateway for managing multiple AI model providers',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function RootLayout({
|
|
11
|
+
children,
|
|
12
|
+
}: {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<html lang="zh-CN">
|
|
17
|
+
<body>
|
|
18
|
+
<ToastProvider>{children}</ToastProvider>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import Nav from '../components/Nav';
|
|
5
|
+
|
|
6
|
+
interface RequestLog {
|
|
7
|
+
id: number;
|
|
8
|
+
model_id: number;
|
|
9
|
+
model_name?: string;
|
|
10
|
+
provider_name?: string;
|
|
11
|
+
request_method: string;
|
|
12
|
+
request_path: string;
|
|
13
|
+
request_headers: string;
|
|
14
|
+
request_query: string;
|
|
15
|
+
request_body: string;
|
|
16
|
+
response_status: number;
|
|
17
|
+
response_body: string;
|
|
18
|
+
response_time_ms: number;
|
|
19
|
+
created_at: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function LogsPage() {
|
|
23
|
+
const [logs, setLogs] = useState<RequestLog[]>([]);
|
|
24
|
+
const [selectedLog, setSelectedLog] = useState<RequestLog | null>(null);
|
|
25
|
+
const [loading, setLoading] = useState(true);
|
|
26
|
+
const [page, setPage] = useState(0);
|
|
27
|
+
const [total, setTotal] = useState(0);
|
|
28
|
+
const limit = 50;
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
loadLogs();
|
|
32
|
+
}, [page]);
|
|
33
|
+
|
|
34
|
+
const loadLogs = async () => {
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`/api/logs?limit=${limit}&offset=${page * limit}`);
|
|
37
|
+
const data = await res.json();
|
|
38
|
+
setLogs(data.logs || []);
|
|
39
|
+
setTotal(data.total || 0);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('Failed to load logs:', error);
|
|
42
|
+
} finally {
|
|
43
|
+
setLoading(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const formatJSON = (jsonString: string) => {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(jsonString);
|
|
50
|
+
return JSON.stringify(parsed, null, 2);
|
|
51
|
+
} catch {
|
|
52
|
+
return jsonString;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const getStatusColor = (status: number) => {
|
|
57
|
+
if (status >= 200 && status < 300) return 'text-green-600';
|
|
58
|
+
if (status >= 400 && status < 500) return 'text-yellow-600';
|
|
59
|
+
if (status >= 500) return 'text-red-600';
|
|
60
|
+
return 'text-gray-600';
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (loading) {
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<Nav />
|
|
67
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
68
|
+
<div className="text-lg">加载中...</div>
|
|
69
|
+
</div>
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<Nav />
|
|
77
|
+
<main className="max-w-6xl mx-auto py-6 sm:px-6 lg:px-8">
|
|
78
|
+
<div className="px-4 py-4 sm:px-0">
|
|
79
|
+
<div className="mb-5">
|
|
80
|
+
<h1 className="text-lg font-bold text-slate-800">请求日志</h1>
|
|
81
|
+
<p className="text-xs text-slate-500 mt-1">查看所有 API 请求记录和响应详情</p>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="bg-white/70 backdrop-blur-sm shadow-md rounded-2xl border border-emerald-100/50 overflow-hidden">
|
|
85
|
+
<table className="min-w-full divide-y divide-slate-100">
|
|
86
|
+
<thead className="bg-emerald-50/30">
|
|
87
|
+
<tr>
|
|
88
|
+
<th className="px-4 py-2.5 text-left text-xs font-semibold text-slate-600 uppercase tracking-wider">
|
|
89
|
+
时间
|
|
90
|
+
</th>
|
|
91
|
+
<th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
|
92
|
+
模型
|
|
93
|
+
</th>
|
|
94
|
+
<th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
|
95
|
+
方法
|
|
96
|
+
</th>
|
|
97
|
+
<th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
|
98
|
+
路径
|
|
99
|
+
</th>
|
|
100
|
+
<th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
|
101
|
+
状态
|
|
102
|
+
</th>
|
|
103
|
+
<th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
|
104
|
+
响应时间
|
|
105
|
+
</th>
|
|
106
|
+
<th className="px-6 py-3.5 text-right text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
|
107
|
+
操作
|
|
108
|
+
</th>
|
|
109
|
+
</tr>
|
|
110
|
+
</thead>
|
|
111
|
+
<tbody className="bg-white divide-y divide-slate-100">
|
|
112
|
+
{logs.length === 0 ? (
|
|
113
|
+
<tr>
|
|
114
|
+
<td colSpan={7} className="px-4 py-12 text-center">
|
|
115
|
+
<div className="text-slate-400">
|
|
116
|
+
<svg className="mx-auto h-10 w-10 mb-3 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
117
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
118
|
+
</svg>
|
|
119
|
+
<p className="text-xs text-slate-500">暂无日志记录</p>
|
|
120
|
+
</div>
|
|
121
|
+
</td>
|
|
122
|
+
</tr>
|
|
123
|
+
) : (
|
|
124
|
+
logs.map((log) => (
|
|
125
|
+
<tr key={log.id} className="hover:bg-emerald-50/20 transition-colors duration-300 cursor-pointer">
|
|
126
|
+
<td className="px-4 py-3 whitespace-nowrap">
|
|
127
|
+
<div className="text-xs text-slate-600">
|
|
128
|
+
{new Date(log.created_at).toLocaleString('zh-CN')}
|
|
129
|
+
</div>
|
|
130
|
+
</td>
|
|
131
|
+
<td className="px-4 py-3 whitespace-nowrap">
|
|
132
|
+
<div className="text-xs">
|
|
133
|
+
<div className="font-medium text-slate-800">{log.model_name || 'Unknown'}</div>
|
|
134
|
+
{log.provider_name && (
|
|
135
|
+
<div className="text-[10px] text-slate-500">{log.provider_name}</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</td>
|
|
139
|
+
<td className="px-4 py-3 whitespace-nowrap">
|
|
140
|
+
<span className={`px-2 py-0.5 inline-flex text-xs leading-4 font-semibold rounded-lg border ${
|
|
141
|
+
log.request_method === 'GET' ? 'bg-sky-50 text-sky-700 border-sky-200/50' :
|
|
142
|
+
log.request_method === 'POST' ? 'bg-emerald-50 text-emerald-700 border-emerald-200/50' :
|
|
143
|
+
'bg-slate-50 text-slate-600 border-slate-200/50'
|
|
144
|
+
}`}>
|
|
145
|
+
{log.request_method}
|
|
146
|
+
</span>
|
|
147
|
+
</td>
|
|
148
|
+
<td className="px-4 py-3">
|
|
149
|
+
<div className="text-xs text-slate-600 font-mono max-w-xs truncate">
|
|
150
|
+
{log.request_path}
|
|
151
|
+
</div>
|
|
152
|
+
</td>
|
|
153
|
+
<td className="px-4 py-3 whitespace-nowrap">
|
|
154
|
+
<span className={`px-2 py-0.5 inline-flex text-xs leading-4 font-semibold rounded-full border ${
|
|
155
|
+
log.response_status >= 200 && log.response_status < 300 ? 'bg-emerald-100/80 text-emerald-700 border-emerald-200/50' :
|
|
156
|
+
log.response_status >= 400 && log.response_status < 500 ? 'bg-amber-100/80 text-amber-700 border-amber-200/50' :
|
|
157
|
+
log.response_status >= 500 ? 'bg-rose-100/80 text-rose-700 border-rose-200/50' :
|
|
158
|
+
'bg-slate-100/80 text-slate-600 border-slate-200/50'
|
|
159
|
+
}`}>
|
|
160
|
+
{log.response_status}
|
|
161
|
+
</span>
|
|
162
|
+
</td>
|
|
163
|
+
<td className="px-4 py-3 whitespace-nowrap">
|
|
164
|
+
<div className="text-xs text-slate-600">
|
|
165
|
+
{log.response_time_ms}ms
|
|
166
|
+
</div>
|
|
167
|
+
</td>
|
|
168
|
+
<td className="px-4 py-3 whitespace-nowrap text-right text-xs font-medium">
|
|
169
|
+
<button
|
|
170
|
+
onClick={() => setSelectedLog(log)}
|
|
171
|
+
className="text-emerald-600 hover:text-emerald-700 font-medium transition-colors duration-300"
|
|
172
|
+
>
|
|
173
|
+
查看详情
|
|
174
|
+
</button>
|
|
175
|
+
</td>
|
|
176
|
+
</tr>
|
|
177
|
+
))
|
|
178
|
+
)}
|
|
179
|
+
</tbody>
|
|
180
|
+
</table>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div className="mt-5 flex items-center justify-between">
|
|
184
|
+
<div className="text-xs text-slate-600">
|
|
185
|
+
共 <span className="font-semibold text-slate-800">{total}</span> 条记录
|
|
186
|
+
</div>
|
|
187
|
+
<div className="flex space-x-2">
|
|
188
|
+
<button
|
|
189
|
+
onClick={() => setPage(Math.max(0, page - 1))}
|
|
190
|
+
disabled={page === 0}
|
|
191
|
+
className="px-3 py-1.5 border border-slate-200 rounded-lg text-xs font-medium text-slate-600 bg-white hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300"
|
|
192
|
+
>
|
|
193
|
+
上一页
|
|
194
|
+
</button>
|
|
195
|
+
<button
|
|
196
|
+
onClick={() => setPage(page + 1)}
|
|
197
|
+
disabled={(page + 1) * limit >= total}
|
|
198
|
+
className="px-3 py-1.5 border border-slate-200 rounded-lg text-xs font-medium text-slate-600 bg-white hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300"
|
|
199
|
+
>
|
|
200
|
+
下一页
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</main>
|
|
206
|
+
|
|
207
|
+
{selectedLog && (
|
|
208
|
+
<div className="fixed z-50 inset-0 overflow-y-auto">
|
|
209
|
+
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
210
|
+
<div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm transition-opacity" onClick={() => setSelectedLog(null)}></div>
|
|
211
|
+
<div className="inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl sm:w-full border border-emerald-100/50">
|
|
212
|
+
<div className="bg-white px-6 pt-5 pb-4 sm:p-6">
|
|
213
|
+
<div className="mb-5">
|
|
214
|
+
<h3 className="text-base font-bold text-slate-800 mb-1">
|
|
215
|
+
请求详情
|
|
216
|
+
</h3>
|
|
217
|
+
<p className="text-xs text-slate-500">查看完整的请求和响应信息</p>
|
|
218
|
+
</div>
|
|
219
|
+
<div className="space-y-4 max-h-[60vh] overflow-y-auto">
|
|
220
|
+
<div>
|
|
221
|
+
<h4 className="text-xs font-semibold text-slate-700 mb-2">请求头</h4>
|
|
222
|
+
<pre className="bg-slate-50/80 border border-slate-200 p-3 rounded-xl text-[10px] overflow-x-auto font-mono text-slate-700">
|
|
223
|
+
{formatJSON(selectedLog.request_headers)}
|
|
224
|
+
</pre>
|
|
225
|
+
</div>
|
|
226
|
+
<div>
|
|
227
|
+
<h4 className="text-xs font-semibold text-slate-700 mb-2">请求 Query</h4>
|
|
228
|
+
<pre className="bg-slate-50/80 border border-slate-200 p-3 rounded-xl text-[10px] overflow-x-auto font-mono text-slate-700">
|
|
229
|
+
{formatJSON(selectedLog.request_query)}
|
|
230
|
+
</pre>
|
|
231
|
+
</div>
|
|
232
|
+
<div>
|
|
233
|
+
<h4 className="text-xs font-semibold text-slate-700 mb-2">请求 Body</h4>
|
|
234
|
+
<pre className="bg-slate-50/80 border border-slate-200 p-3 rounded-xl text-[10px] overflow-x-auto font-mono text-slate-700">
|
|
235
|
+
{formatJSON(selectedLog.request_body)}
|
|
236
|
+
</pre>
|
|
237
|
+
</div>
|
|
238
|
+
<div>
|
|
239
|
+
<h4 className="text-xs font-semibold text-slate-700 mb-2">响应 Body</h4>
|
|
240
|
+
<pre className="bg-slate-50/80 border border-slate-200 p-3 rounded-xl text-[10px] overflow-x-auto font-mono text-slate-700">
|
|
241
|
+
{formatJSON(selectedLog.response_body)}
|
|
242
|
+
</pre>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
<div className="mt-5 flex justify-end">
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
onClick={() => setSelectedLog(null)}
|
|
249
|
+
className="px-4 py-1.5 border border-slate-200 rounded-lg text-xs font-semibold text-slate-600 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-slate-400/30 transition-all duration-300"
|
|
250
|
+
>
|
|
251
|
+
关闭
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
</>
|
|
260
|
+
);
|
|
261
|
+
}
|