aicodeswitch 1.4.1 → 1.5.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.md +1 -0
- package/dist/server/version-check.js +1 -2
- package/package.json +2 -2
- package/src/server/auth.ts +79 -0
- package/src/server/database.ts +809 -0
- package/src/server/main.ts +514 -0
- package/src/server/proxy-server.ts +1301 -0
- package/src/server/transformers/chunk-collector.ts +202 -0
- package/src/server/transformers/claude-openai.ts +261 -0
- package/src/server/transformers/openai-responses.ts +440 -0
- package/src/server/transformers/streaming.ts +775 -0
- package/src/server/version-check.ts +108 -0
- package/src/types/index.ts +217 -0
- package/src/ui/App.tsx +342 -0
- package/src/ui/api/client.ts +179 -0
- package/src/ui/components/JSONViewer.tsx +89 -0
- package/src/ui/constants/index.ts +4 -0
- package/src/ui/docs/vendors-recommand.md +13 -0
- package/src/ui/main.tsx +10 -0
- package/src/ui/pages/LogsPage.tsx +702 -0
- package/src/ui/pages/RoutesPage.tsx +552 -0
- package/src/ui/pages/SettingsPage.tsx +206 -0
- package/src/ui/pages/StatisticsPage.tsx +620 -0
- package/src/ui/pages/UsagePage.tsx +13 -0
- package/src/ui/pages/VendorsPage.tsx +490 -0
- package/src/ui/pages/WriteConfigPage.tsx +198 -0
- package/src/ui/styles/App.css +831 -0
- package/src/ui/styles/index.css +137 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
const PACKAGE_NAME = 'aicodeswitch';
|
|
6
|
+
const NPM_REGISTRY = 'registry.npmjs.org';
|
|
7
|
+
|
|
8
|
+
// 比较版本号
|
|
9
|
+
export const compareVersions = (v1: string, v2: string): number => {
|
|
10
|
+
const parts1 = v1.split('.').map(Number);
|
|
11
|
+
const parts2 = v2.split('.').map(Number);
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < 3; i++) {
|
|
14
|
+
if (parts1[i] > parts2[i]) return 1;
|
|
15
|
+
if (parts1[i] < parts2[i]) return -1;
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// 获取当前版本
|
|
21
|
+
export const getCurrentVersion = (): string | null => {
|
|
22
|
+
try {
|
|
23
|
+
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
|
24
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
25
|
+
return packageJson.version;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error('Failed to read current version:', err);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// 从 npm 获取最新版本
|
|
33
|
+
export const getLatestVersion = (): Promise<string> => {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const options = {
|
|
36
|
+
hostname: NPM_REGISTRY,
|
|
37
|
+
path: `/${PACKAGE_NAME}`,
|
|
38
|
+
method: 'GET',
|
|
39
|
+
headers: {
|
|
40
|
+
'User-Agent': 'aicodeswitch-version-check'
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const req = https.request(options, (res) => {
|
|
45
|
+
let data = '';
|
|
46
|
+
|
|
47
|
+
res.on('data', (chunk) => {
|
|
48
|
+
data += chunk;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
res.on('end', () => {
|
|
52
|
+
try {
|
|
53
|
+
const packageInfo = JSON.parse(data);
|
|
54
|
+
resolve(packageInfo['dist-tags'].latest);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
reject(new Error('Failed to parse npm response'));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
req.on('error', (err) => {
|
|
62
|
+
reject(err);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
req.setTimeout(10000, () => {
|
|
66
|
+
req.destroy();
|
|
67
|
+
reject(new Error('Request timeout'));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
req.end();
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// 检查版本更新
|
|
75
|
+
export const checkVersionUpdate = async (): Promise<{
|
|
76
|
+
hasUpdate: boolean;
|
|
77
|
+
currentVersion: string | null;
|
|
78
|
+
latestVersion: string | null;
|
|
79
|
+
}> => {
|
|
80
|
+
try {
|
|
81
|
+
const currentVersion = getCurrentVersion();
|
|
82
|
+
const latestVersion = await getLatestVersion();
|
|
83
|
+
|
|
84
|
+
if (!currentVersion || !latestVersion) {
|
|
85
|
+
return {
|
|
86
|
+
hasUpdate: false,
|
|
87
|
+
currentVersion,
|
|
88
|
+
latestVersion
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const versionCompare = compareVersions(latestVersion, currentVersion);
|
|
93
|
+
const hasUpdate = versionCompare > 0;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
hasUpdate,
|
|
97
|
+
currentVersion,
|
|
98
|
+
latestVersion
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('Failed to check version update:', error);
|
|
102
|
+
return {
|
|
103
|
+
hasUpdate: false,
|
|
104
|
+
currentVersion: getCurrentVersion(),
|
|
105
|
+
latestVersion: null
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/** 供应商信息 */
|
|
2
|
+
export interface Vendor {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
createdAt: number;
|
|
7
|
+
updatedAt: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** 供应商API接口的数据结构标准类型 */
|
|
11
|
+
export type SourceType = 'openai-chat' | 'openai-code' | 'openai-responses' | 'claude-chat' | 'claude-code' | 'deepseek-chat';
|
|
12
|
+
/** 路由的目标对象类型,目前,仅支持claude-code和codex */
|
|
13
|
+
export type TargetType = 'claude-code' | 'codex';
|
|
14
|
+
|
|
15
|
+
/** 供应商API服务 */
|
|
16
|
+
export interface APIService {
|
|
17
|
+
id: string;
|
|
18
|
+
vendorId: string;
|
|
19
|
+
name: string;
|
|
20
|
+
apiUrl: string;
|
|
21
|
+
apiKey: string;
|
|
22
|
+
timeout?: number;
|
|
23
|
+
sourceType?: SourceType;
|
|
24
|
+
supportedModels?: string[];
|
|
25
|
+
createdAt: number;
|
|
26
|
+
updatedAt: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 路由信息 */
|
|
30
|
+
export interface Route {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
targetType: TargetType;
|
|
35
|
+
isActive: boolean;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
updatedAt: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 路由规则 */
|
|
41
|
+
export interface Rule {
|
|
42
|
+
id: string;
|
|
43
|
+
routeId: string;
|
|
44
|
+
contentType: ContentType;
|
|
45
|
+
targetServiceId: string;
|
|
46
|
+
targetModel?: string;
|
|
47
|
+
replacedModel?: string;
|
|
48
|
+
sortOrder?: number;
|
|
49
|
+
createdAt: number;
|
|
50
|
+
updatedAt: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type ContentType = 'default' | 'background' | 'thinking' | 'long-context' | 'image-understanding' | 'model-mapping';
|
|
54
|
+
|
|
55
|
+
export interface RequestLog {
|
|
56
|
+
id: string;
|
|
57
|
+
timestamp: number;
|
|
58
|
+
method: string;
|
|
59
|
+
path: string;
|
|
60
|
+
headers: Record<string, string>;
|
|
61
|
+
body?: string;
|
|
62
|
+
statusCode?: number;
|
|
63
|
+
responseTime?: number;
|
|
64
|
+
targetProvider?: string;
|
|
65
|
+
usage?: TokenUsage;
|
|
66
|
+
error?: string;
|
|
67
|
+
|
|
68
|
+
// 新增字段 - 用于日志筛选和详情展示
|
|
69
|
+
targetType?: TargetType; // 来源对象类型
|
|
70
|
+
targetServiceId?: string; // API服务ID
|
|
71
|
+
targetServiceName?: string; // API服务名
|
|
72
|
+
targetModel?: string; // 模型名
|
|
73
|
+
vendorId?: string; // 供应商ID
|
|
74
|
+
vendorName?: string; // 供应商名称
|
|
75
|
+
requestModel?: string; // 请求模型名(从请求体中读取)
|
|
76
|
+
|
|
77
|
+
responseHeaders?: Record<string, string>; // 响应头
|
|
78
|
+
responseBody?: string; // 响应体(非stream)
|
|
79
|
+
streamChunks?: string[]; // stream chunks数组
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface AccessLog {
|
|
83
|
+
id: string;
|
|
84
|
+
timestamp: number;
|
|
85
|
+
method: string;
|
|
86
|
+
path: string;
|
|
87
|
+
clientIp?: string;
|
|
88
|
+
userAgent?: string;
|
|
89
|
+
statusCode?: number;
|
|
90
|
+
responseTime?: number;
|
|
91
|
+
error?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ErrorLog {
|
|
95
|
+
id: string;
|
|
96
|
+
timestamp: number;
|
|
97
|
+
method: string;
|
|
98
|
+
path: string;
|
|
99
|
+
statusCode?: number;
|
|
100
|
+
errorMessage: string;
|
|
101
|
+
errorStack?: string;
|
|
102
|
+
requestHeaders?: Record<string, string>;
|
|
103
|
+
requestBody?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface AppConfig {
|
|
107
|
+
enableLogging: boolean;
|
|
108
|
+
logRetentionDays: number;
|
|
109
|
+
maxLogSize: number;
|
|
110
|
+
apiKey: string;
|
|
111
|
+
enableFailover?: boolean; // 是否启用智能故障切换,默认 true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface ExportData {
|
|
115
|
+
version: string;
|
|
116
|
+
exportDate: number;
|
|
117
|
+
vendors: Vendor[];
|
|
118
|
+
apiServices: APIService[];
|
|
119
|
+
routes: Route[];
|
|
120
|
+
rules: Rule[];
|
|
121
|
+
config: AppConfig;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface TokenUsage {
|
|
125
|
+
inputTokens: number;
|
|
126
|
+
outputTokens: number;
|
|
127
|
+
totalTokens?: number;
|
|
128
|
+
cacheReadInputTokens?: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 服务黑名单记录 */
|
|
132
|
+
export interface ServiceBlacklistEntry {
|
|
133
|
+
serviceId: string;
|
|
134
|
+
routeId: string;
|
|
135
|
+
contentType: ContentType;
|
|
136
|
+
blacklistedAt: number; // 标记时间戳
|
|
137
|
+
expiresAt: number; // 过期时间 = blacklistedAt + 10分钟
|
|
138
|
+
errorCount: number; // 错误计数
|
|
139
|
+
lastError?: string; // 最后一次错误信息
|
|
140
|
+
lastStatusCode?: number; // 最后一次错误的状态码
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 鉴权状态响应 */
|
|
144
|
+
export interface AuthStatus {
|
|
145
|
+
enabled: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 登录请求 */
|
|
149
|
+
export interface LoginRequest {
|
|
150
|
+
authCode: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 登录响应 */
|
|
154
|
+
export interface LoginResponse {
|
|
155
|
+
token: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 统计数据 */
|
|
159
|
+
export interface Statistics {
|
|
160
|
+
overview: {
|
|
161
|
+
totalRequests: number;
|
|
162
|
+
totalTokens: number;
|
|
163
|
+
totalInputTokens: number;
|
|
164
|
+
totalOutputTokens: number;
|
|
165
|
+
totalCacheReadTokens: number;
|
|
166
|
+
totalVendors: number;
|
|
167
|
+
totalServices: number;
|
|
168
|
+
totalRoutes: number;
|
|
169
|
+
totalRules: number;
|
|
170
|
+
avgResponseTime: number;
|
|
171
|
+
successRate: number;
|
|
172
|
+
totalCodingTime: number; // 编程时长(分钟)
|
|
173
|
+
};
|
|
174
|
+
byTargetType: {
|
|
175
|
+
targetType: TargetType;
|
|
176
|
+
totalRequests: number;
|
|
177
|
+
totalTokens: number;
|
|
178
|
+
avgResponseTime: number;
|
|
179
|
+
}[];
|
|
180
|
+
byVendor: {
|
|
181
|
+
vendorId: string;
|
|
182
|
+
vendorName: string;
|
|
183
|
+
totalRequests: number;
|
|
184
|
+
totalTokens: number;
|
|
185
|
+
avgResponseTime: number;
|
|
186
|
+
}[];
|
|
187
|
+
byService: {
|
|
188
|
+
serviceId: string;
|
|
189
|
+
serviceName: string;
|
|
190
|
+
vendorName: string;
|
|
191
|
+
totalRequests: number;
|
|
192
|
+
totalTokens: number;
|
|
193
|
+
avgResponseTime: number;
|
|
194
|
+
}[];
|
|
195
|
+
byModel: {
|
|
196
|
+
modelName: string;
|
|
197
|
+
totalRequests: number;
|
|
198
|
+
totalTokens: number;
|
|
199
|
+
avgResponseTime: number;
|
|
200
|
+
}[];
|
|
201
|
+
timeline: {
|
|
202
|
+
date: string; // YYYY-MM-DD
|
|
203
|
+
totalRequests: number;
|
|
204
|
+
totalTokens: number;
|
|
205
|
+
totalInputTokens: number;
|
|
206
|
+
totalOutputTokens: number;
|
|
207
|
+
}[];
|
|
208
|
+
contentTypeDistribution: {
|
|
209
|
+
contentType: string;
|
|
210
|
+
count: number;
|
|
211
|
+
percentage: number;
|
|
212
|
+
}[];
|
|
213
|
+
errors: {
|
|
214
|
+
totalErrors: number;
|
|
215
|
+
recentErrors: number; // 最近24小时
|
|
216
|
+
};
|
|
217
|
+
}
|
package/src/ui/App.tsx
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { HashRouter as Router, Routes, Route, NavLink, useNavigate } from 'react-router-dom';
|
|
3
|
+
import { api } from './api/client';
|
|
4
|
+
import VendorsPage from './pages/VendorsPage';
|
|
5
|
+
import RouteGroupsPage from './pages/RoutesPage';
|
|
6
|
+
import LogsPage from './pages/LogsPage';
|
|
7
|
+
import SettingsPage from './pages/SettingsPage';
|
|
8
|
+
import WriteConfigPage from './pages/WriteConfigPage';
|
|
9
|
+
import UsagePage from './pages/UsagePage';
|
|
10
|
+
import StatisticsPage from './pages/StatisticsPage';
|
|
11
|
+
import './styles/App.css';
|
|
12
|
+
|
|
13
|
+
function AppContent() {
|
|
14
|
+
const navigate = useNavigate();
|
|
15
|
+
const [theme, setTheme] = useState('light');
|
|
16
|
+
const [showVendorModal, setShowVendorModal] = useState(false);
|
|
17
|
+
const [hasCheckedVendors, setHasCheckedVendors] = useState(false);
|
|
18
|
+
|
|
19
|
+
// 版本更新相关状态
|
|
20
|
+
const [hasUpdate, setHasUpdate] = useState(false);
|
|
21
|
+
const [latestVersion, setLatestVersion] = useState<string | null>(null);
|
|
22
|
+
const [currentVersion, setCurrentVersion] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
// 鉴权相关状态
|
|
25
|
+
const [authEnabled, setAuthEnabled] = useState(false);
|
|
26
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
27
|
+
const [authCode, setAuthCode] = useState('');
|
|
28
|
+
const [loginError, setLoginError] = useState('');
|
|
29
|
+
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
|
33
|
+
setTheme(savedTheme);
|
|
34
|
+
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
// 版本检查 - 每1分钟检查一次
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const checkVersion = async () => {
|
|
40
|
+
try {
|
|
41
|
+
const versionInfo = await api.checkVersion();
|
|
42
|
+
setHasUpdate(versionInfo.hasUpdate);
|
|
43
|
+
setLatestVersion(versionInfo.latestVersion);
|
|
44
|
+
setCurrentVersion(versionInfo.currentVersion);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Failed to check version:', error);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 立即检查一次
|
|
51
|
+
checkVersion();
|
|
52
|
+
|
|
53
|
+
// 每1分钟检查一次
|
|
54
|
+
const intervalId = setInterval(checkVersion, 60000);
|
|
55
|
+
|
|
56
|
+
return () => clearInterval(intervalId);
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
// 检查鉴权状态
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const checkAuth = async () => {
|
|
62
|
+
try {
|
|
63
|
+
// 1. 检查是否启用鉴权
|
|
64
|
+
const authStatus = await api.getAuthStatus();
|
|
65
|
+
setAuthEnabled(authStatus.enabled);
|
|
66
|
+
|
|
67
|
+
if (!authStatus.enabled) {
|
|
68
|
+
// 未启用鉴权,直接标记为已认证
|
|
69
|
+
setIsAuthenticated(true);
|
|
70
|
+
setIsCheckingAuth(false);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. 如果启用鉴权,检查本地是否有 token
|
|
75
|
+
const token = localStorage.getItem('auth_token');
|
|
76
|
+
if (!token) {
|
|
77
|
+
// 没有 token,不设置为已认证
|
|
78
|
+
setIsCheckingAuth(false);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 3. 有 token,尝试调用 API 验证 token 有效性
|
|
83
|
+
try {
|
|
84
|
+
await api.getVendors(); // 调用任意需要鉴权的 API
|
|
85
|
+
setIsAuthenticated(true);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// Token 无效,不设置为已认证
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('Failed to check auth status:', error);
|
|
91
|
+
} finally {
|
|
92
|
+
setIsCheckingAuth(false);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
checkAuth();
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const checkVendors = async () => {
|
|
101
|
+
try {
|
|
102
|
+
const vendors = await api.getVendors();
|
|
103
|
+
if (vendors.length === 0 && !hasCheckedVendors) {
|
|
104
|
+
setShowVendorModal(true);
|
|
105
|
+
setHasCheckedVendors(true);
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('Failed to check vendors:', error);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
checkVendors();
|
|
113
|
+
}, [hasCheckedVendors]);
|
|
114
|
+
|
|
115
|
+
const toggleTheme = () => {
|
|
116
|
+
const newTheme = theme === 'light' ? 'dark' : 'light';
|
|
117
|
+
setTheme(newTheme);
|
|
118
|
+
document.documentElement.setAttribute('data-theme', newTheme);
|
|
119
|
+
localStorage.setItem('theme', newTheme);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleVendorModalConfirm = () => {
|
|
123
|
+
setShowVendorModal(false);
|
|
124
|
+
navigate('/vendors');
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const handleLogin = async (e: React.FormEvent) => {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
setLoginError('');
|
|
130
|
+
|
|
131
|
+
if (!authCode.trim()) {
|
|
132
|
+
setLoginError('请输入鉴权码');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await api.login(authCode);
|
|
138
|
+
localStorage.setItem('auth_token', response.token);
|
|
139
|
+
setIsAuthenticated(true);
|
|
140
|
+
setAuthCode('');
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof Error) {
|
|
143
|
+
setLoginError(error.message || '登录失败,请检查鉴权码');
|
|
144
|
+
} else {
|
|
145
|
+
setLoginError('登录失败,请检查鉴权码');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// 如果正在检查鉴权状态,显示加载中
|
|
151
|
+
if (isCheckingAuth) {
|
|
152
|
+
return (
|
|
153
|
+
<div style={{
|
|
154
|
+
display: 'flex',
|
|
155
|
+
justifyContent: 'center',
|
|
156
|
+
alignItems: 'center',
|
|
157
|
+
height: '100vh',
|
|
158
|
+
fontSize: '18px',
|
|
159
|
+
color: 'var(--text-secondary)'
|
|
160
|
+
}}>
|
|
161
|
+
加载中...
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 如果启用鉴权且未认证,只显示登录弹层
|
|
167
|
+
if (authEnabled && !isAuthenticated) {
|
|
168
|
+
return (
|
|
169
|
+
<div className="modal-overlay">
|
|
170
|
+
<div className="modal" style={{ maxWidth: '400px' }}>
|
|
171
|
+
<div className="modal-header">
|
|
172
|
+
<h2>🔐 系统鉴权</h2>
|
|
173
|
+
</div>
|
|
174
|
+
<form onSubmit={handleLogin}>
|
|
175
|
+
<div style={{ padding: '20px 0' }}>
|
|
176
|
+
<p style={{ marginBottom: '16px', lineHeight: '1.6', color: 'var(--text-secondary)' }}>
|
|
177
|
+
系统已启用鉴权保护,请输入鉴权码以继续访问。
|
|
178
|
+
</p>
|
|
179
|
+
<div style={{ marginBottom: '16px' }}>
|
|
180
|
+
<label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
|
|
181
|
+
鉴权码
|
|
182
|
+
</label>
|
|
183
|
+
<input
|
|
184
|
+
type="password"
|
|
185
|
+
value={authCode}
|
|
186
|
+
onChange={(e) => setAuthCode(e.target.value)}
|
|
187
|
+
placeholder="请输入鉴权码"
|
|
188
|
+
style={{
|
|
189
|
+
width: '100%',
|
|
190
|
+
padding: '10px 12px',
|
|
191
|
+
border: '1px solid var(--border-color)',
|
|
192
|
+
borderRadius: '6px',
|
|
193
|
+
fontSize: '14px',
|
|
194
|
+
boxSizing: 'border-box'
|
|
195
|
+
}}
|
|
196
|
+
autoFocus
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
{loginError && (
|
|
200
|
+
<div style={{
|
|
201
|
+
padding: '10px 12px',
|
|
202
|
+
backgroundColor: '#fee',
|
|
203
|
+
border: '1px solid #fcc',
|
|
204
|
+
borderRadius: '6px',
|
|
205
|
+
color: '#c33',
|
|
206
|
+
fontSize: '14px',
|
|
207
|
+
marginBottom: '16px'
|
|
208
|
+
}}>
|
|
209
|
+
{loginError}
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
<div className="modal-footer">
|
|
214
|
+
<button type="submit" className="btn btn-primary" style={{ width: '100%' }}>
|
|
215
|
+
登录
|
|
216
|
+
</button>
|
|
217
|
+
</div>
|
|
218
|
+
</form>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<div className="app">
|
|
226
|
+
<nav className="sidebar">
|
|
227
|
+
<div className="logo">
|
|
228
|
+
<h2>AI Code Switch</h2>
|
|
229
|
+
</div>
|
|
230
|
+
<ul className="nav-menu">
|
|
231
|
+
<li>
|
|
232
|
+
<NavLink to="/">📊 数据统计</NavLink>
|
|
233
|
+
</li>
|
|
234
|
+
<li>
|
|
235
|
+
<NavLink to="/routes">路由管理</NavLink>
|
|
236
|
+
</li>
|
|
237
|
+
<li>
|
|
238
|
+
<NavLink to="/vendors">供应商管理</NavLink>
|
|
239
|
+
</li>
|
|
240
|
+
<li>
|
|
241
|
+
<NavLink to="/write-config">覆盖配置文件</NavLink>
|
|
242
|
+
</li>
|
|
243
|
+
<li>
|
|
244
|
+
<NavLink to="/logs">请求日志</NavLink>
|
|
245
|
+
</li>
|
|
246
|
+
<li>
|
|
247
|
+
<NavLink to="/settings">设置</NavLink>
|
|
248
|
+
</li>
|
|
249
|
+
<li>
|
|
250
|
+
<NavLink to="/usage">使用说明</NavLink>
|
|
251
|
+
</li>
|
|
252
|
+
</ul>
|
|
253
|
+
|
|
254
|
+
{hasUpdate && (
|
|
255
|
+
<div className="update-notification">
|
|
256
|
+
<div className="update-notification-content">
|
|
257
|
+
<span className="update-icon">⬆️</span>
|
|
258
|
+
<div className="update-text">
|
|
259
|
+
<div className="update-title">新版本可用</div>
|
|
260
|
+
<div className="update-versions">
|
|
261
|
+
{currentVersion} → {latestVersion}
|
|
262
|
+
</div>
|
|
263
|
+
<div className="update-message">
|
|
264
|
+
命令行执行如下更新到最新版本<br />
|
|
265
|
+
<code>npm i -g aicodeswitch</code>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
<a
|
|
270
|
+
href="https://npmjs.com/package/aicodeswitch"
|
|
271
|
+
target="_blank"
|
|
272
|
+
rel="noopener noreferrer"
|
|
273
|
+
className="update-link"
|
|
274
|
+
>
|
|
275
|
+
查看详情
|
|
276
|
+
</a>
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
|
|
280
|
+
<div className="theme-toggle">
|
|
281
|
+
<button
|
|
282
|
+
onClick={toggleTheme}
|
|
283
|
+
className={theme === 'light' ? 'active' : ''}
|
|
284
|
+
title="浅色模式"
|
|
285
|
+
>
|
|
286
|
+
☀️
|
|
287
|
+
</button>
|
|
288
|
+
<button
|
|
289
|
+
onClick={toggleTheme}
|
|
290
|
+
className={theme === 'dark' ? 'active' : ''}
|
|
291
|
+
title="深色模式"
|
|
292
|
+
>
|
|
293
|
+
🌙
|
|
294
|
+
</button>
|
|
295
|
+
</div>
|
|
296
|
+
</nav>
|
|
297
|
+
<main className="main-content">
|
|
298
|
+
<Routes>
|
|
299
|
+
<Route path="/" element={<StatisticsPage />} />
|
|
300
|
+
<Route path="/routes" element={<RouteGroupsPage />} />
|
|
301
|
+
<Route path="/vendors" element={<VendorsPage />} />
|
|
302
|
+
<Route path="/logs" element={<LogsPage />} />
|
|
303
|
+
<Route path="/write-config" element={<WriteConfigPage />} />
|
|
304
|
+
<Route path="/settings" element={<SettingsPage />} />
|
|
305
|
+
<Route path="/usage" element={<UsagePage />} />
|
|
306
|
+
</Routes>
|
|
307
|
+
</main>
|
|
308
|
+
|
|
309
|
+
{showVendorModal && (
|
|
310
|
+
<div className="modal-overlay" onClick={() => setShowVendorModal(false)}>
|
|
311
|
+
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
|
312
|
+
<div className="modal-header">
|
|
313
|
+
<h2>⚠️ 需要配置供应商</h2>
|
|
314
|
+
</div>
|
|
315
|
+
<div style={{ padding: '20px 0' }}>
|
|
316
|
+
<p style={{ marginBottom: '16px', lineHeight: '1.6' }}>
|
|
317
|
+
检测到系统中没有配置任何API供应商。在没有供应商的情况下,路由将无法正常工作。
|
|
318
|
+
</p>
|
|
319
|
+
<p style={{ marginBottom: '0', lineHeight: '1.6', fontWeight: '500' }}>
|
|
320
|
+
请先添加至少一个供应商,然后再配置路由规则。
|
|
321
|
+
</p>
|
|
322
|
+
</div>
|
|
323
|
+
<div className="modal-footer">
|
|
324
|
+
<button type="button" className="btn btn-secondary" onClick={() => setShowVendorModal(false)}>稍后</button>
|
|
325
|
+
<button type="button" className="btn btn-primary" onClick={handleVendorModalConfirm}>前往供应商管理</button>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function App() {
|
|
335
|
+
return (
|
|
336
|
+
<Router>
|
|
337
|
+
<AppContent />
|
|
338
|
+
</Router>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export default App;
|