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,179 @@
|
|
|
1
|
+
import type { Vendor, APIService, Route, Rule, RequestLog, AccessLog, ErrorLog, AppConfig, AuthStatus, LoginResponse, Statistics } from '../../types';
|
|
2
|
+
|
|
3
|
+
interface BackendAPI {
|
|
4
|
+
// 鉴权相关
|
|
5
|
+
getAuthStatus: () => Promise<AuthStatus>;
|
|
6
|
+
login: (authCode: string) => Promise<LoginResponse>;
|
|
7
|
+
|
|
8
|
+
// 版本检查
|
|
9
|
+
checkVersion: () => Promise<{ hasUpdate: boolean; currentVersion: string | null; latestVersion: string | null }>;
|
|
10
|
+
|
|
11
|
+
getVendors: () => Promise<Vendor[]>;
|
|
12
|
+
createVendor: (vendor: Omit<Vendor, 'id' | 'createdAt' | 'updatedAt'>) => Promise<Vendor>;
|
|
13
|
+
updateVendor: (id: string, vendor: Partial<Vendor>) => Promise<boolean>;
|
|
14
|
+
deleteVendor: (id: string) => Promise<boolean>;
|
|
15
|
+
|
|
16
|
+
getAPIServices: (vendorId?: string) => Promise<APIService[]>;
|
|
17
|
+
createAPIService: (service: Omit<APIService, 'id' | 'createdAt' | 'updatedAt'>) => Promise<APIService>;
|
|
18
|
+
updateAPIService: (id: string, service: Partial<APIService>) => Promise<boolean>;
|
|
19
|
+
deleteAPIService: (id: string) => Promise<boolean>;
|
|
20
|
+
|
|
21
|
+
getRoutes: () => Promise<Route[]>;
|
|
22
|
+
createRoute: (group: Omit<Route, 'id' | 'createdAt' | 'updatedAt'>) => Promise<Route>;
|
|
23
|
+
updateRoute: (id: string, group: Partial<Route>) => Promise<boolean>;
|
|
24
|
+
deleteRoute: (id: string) => Promise<boolean>;
|
|
25
|
+
activateRoute: (id: string) => Promise<boolean>;
|
|
26
|
+
deactivateRoute: (id: string) => Promise<boolean>;
|
|
27
|
+
|
|
28
|
+
getRules: (routeId?: string) => Promise<Rule[]>;
|
|
29
|
+
createRule: (route: Omit<Rule, 'id' | 'createdAt' | 'updatedAt'>) => Promise<Rule>;
|
|
30
|
+
updateRule: (id: string, route: Partial<Rule>) => Promise<boolean>;
|
|
31
|
+
deleteRule: (id: string) => Promise<boolean>;
|
|
32
|
+
|
|
33
|
+
getLogs: (limit: number, offset: number) => Promise<RequestLog[]>;
|
|
34
|
+
clearLogs: () => Promise<boolean>;
|
|
35
|
+
|
|
36
|
+
getAccessLogs: (limit: number, offset: number) => Promise<AccessLog[]>;
|
|
37
|
+
clearAccessLogs: () => Promise<boolean>;
|
|
38
|
+
|
|
39
|
+
getErrorLogs: (limit: number, offset: number) => Promise<ErrorLog[]>;
|
|
40
|
+
clearErrorLogs: () => Promise<boolean>;
|
|
41
|
+
|
|
42
|
+
getConfig: () => Promise<AppConfig>;
|
|
43
|
+
updateConfig: (config: AppConfig) => Promise<boolean>;
|
|
44
|
+
|
|
45
|
+
exportData: (password: string) => Promise<string>;
|
|
46
|
+
importData: (encryptedData: string, password: string) => Promise<boolean>;
|
|
47
|
+
|
|
48
|
+
writeClaudeConfig: () => Promise<boolean>;
|
|
49
|
+
writeCodexConfig: () => Promise<boolean>;
|
|
50
|
+
restoreClaudeConfig: () => Promise<boolean>;
|
|
51
|
+
restoreCodexConfig: () => Promise<boolean>;
|
|
52
|
+
checkClaudeBackup: () => Promise<{ exists: boolean }>;
|
|
53
|
+
checkCodexBackup: () => Promise<{ exists: boolean }>;
|
|
54
|
+
|
|
55
|
+
getStatistics: (days?: number) => Promise<Statistics>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const buildUrl = (
|
|
59
|
+
path: string,
|
|
60
|
+
query?: Record<string, string | number | undefined>
|
|
61
|
+
): string => {
|
|
62
|
+
if (!query) {
|
|
63
|
+
return path;
|
|
64
|
+
}
|
|
65
|
+
const params = new URLSearchParams();
|
|
66
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
67
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
68
|
+
params.set(key, String(value));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const queryString = params.toString();
|
|
72
|
+
return queryString ? `${path}?${queryString}` : path;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const requestJson = async <T>(path: string, options: RequestInit = {}): Promise<T> => {
|
|
76
|
+
// 从 localStorage 读取 token
|
|
77
|
+
const token = localStorage.getItem('auth_token');
|
|
78
|
+
|
|
79
|
+
const response = await fetch(path, {
|
|
80
|
+
...options,
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
84
|
+
...(options.headers || {}),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
// 如果是 401,清除本地 token 并抛出特殊错误
|
|
90
|
+
if (response.status === 401) {
|
|
91
|
+
localStorage.removeItem('auth_token');
|
|
92
|
+
const message = await response.text();
|
|
93
|
+
const error = new Error(message || response.statusText) as Error & { status: number };
|
|
94
|
+
error.status = 401;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const message = await response.text();
|
|
99
|
+
throw new Error(message || response.statusText);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (response.status === 204) {
|
|
103
|
+
return undefined as T;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const contentType = response.headers.get('content-type') || '';
|
|
107
|
+
if (contentType.includes('application/json')) {
|
|
108
|
+
return response.json() as Promise<T>;
|
|
109
|
+
}
|
|
110
|
+
return response.text() as Promise<T>;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const api: BackendAPI = {
|
|
114
|
+
// 鉴权相关
|
|
115
|
+
getAuthStatus: () => requestJson(buildUrl('/api/auth/status')),
|
|
116
|
+
login: (authCode) => requestJson(buildUrl('/api/auth/login'), {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
body: JSON.stringify({ authCode })
|
|
119
|
+
}),
|
|
120
|
+
|
|
121
|
+
// 版本检查
|
|
122
|
+
checkVersion: () => requestJson(buildUrl('/api/version/check')),
|
|
123
|
+
|
|
124
|
+
getVendors: () => requestJson(buildUrl('/api/vendors')),
|
|
125
|
+
createVendor: (vendor) => requestJson(buildUrl('/api/vendors'), { method: 'POST', body: JSON.stringify(vendor) }),
|
|
126
|
+
updateVendor: (id, vendor) => requestJson(buildUrl(`/api/vendors/${id}`), { method: 'PUT', body: JSON.stringify(vendor) }),
|
|
127
|
+
deleteVendor: (id) => requestJson(buildUrl(`/api/vendors/${id}`), { method: 'DELETE' }),
|
|
128
|
+
|
|
129
|
+
getAPIServices: (vendorId) => requestJson(buildUrl('/api/services', vendorId ? { vendorId } : undefined)),
|
|
130
|
+
createAPIService: (service) => requestJson(buildUrl('/api/services'), { method: 'POST', body: JSON.stringify(service) }),
|
|
131
|
+
updateAPIService: (id, service) => requestJson(buildUrl(`/api/services/${id}`), { method: 'PUT', body: JSON.stringify(service) }),
|
|
132
|
+
deleteAPIService: (id) => requestJson(buildUrl(`/api/services/${id}`), { method: 'DELETE' }),
|
|
133
|
+
|
|
134
|
+
getRoutes: () => requestJson(buildUrl('/api/routes')),
|
|
135
|
+
createRoute: (group) => requestJson(buildUrl('/api/routes'), { method: 'POST', body: JSON.stringify(group) }),
|
|
136
|
+
updateRoute: (id, group) => requestJson(buildUrl(`/api/routes/${id}`), { method: 'PUT', body: JSON.stringify(group) }),
|
|
137
|
+
deleteRoute: (id) => requestJson(buildUrl(`/api/routes/${id}`), { method: 'DELETE' }),
|
|
138
|
+
activateRoute: (id) => requestJson(buildUrl(`/api/routes/${id}/activate`), { method: 'POST' }),
|
|
139
|
+
deactivateRoute: (id) => requestJson(buildUrl(`/api/routes/${id}/deactivate`), { method: 'POST' }),
|
|
140
|
+
|
|
141
|
+
getRules: (routeId) => requestJson(buildUrl('/api/rules', routeId ? { routeId } : undefined)),
|
|
142
|
+
createRule: (route) => requestJson(buildUrl('/api/rules'), { method: 'POST', body: JSON.stringify(route) }),
|
|
143
|
+
updateRule: (id, route) => requestJson(buildUrl(`/api/rules/${id}`), { method: 'PUT', body: JSON.stringify(route) }),
|
|
144
|
+
deleteRule: (id) => requestJson(buildUrl(`/api/rules/${id}`), { method: 'DELETE' }),
|
|
145
|
+
|
|
146
|
+
getLogs: (limit, offset) => requestJson(buildUrl('/api/logs', { limit, offset })),
|
|
147
|
+
clearLogs: () => requestJson(buildUrl('/api/logs'), { method: 'DELETE' }),
|
|
148
|
+
|
|
149
|
+
getAccessLogs: (limit, offset) => requestJson(buildUrl('/api/access-logs', { limit, offset })),
|
|
150
|
+
clearAccessLogs: () => requestJson(buildUrl('/api/access-logs'), { method: 'DELETE' }),
|
|
151
|
+
|
|
152
|
+
getErrorLogs: (limit, offset) => requestJson(buildUrl('/api/error-logs', { limit, offset })),
|
|
153
|
+
clearErrorLogs: () => requestJson(buildUrl('/api/error-logs'), { method: 'DELETE' }),
|
|
154
|
+
|
|
155
|
+
getConfig: () => requestJson(buildUrl('/api/config')),
|
|
156
|
+
updateConfig: (config) => requestJson(buildUrl('/api/config'), { method: 'PUT', body: JSON.stringify(config) }),
|
|
157
|
+
|
|
158
|
+
exportData: async (password) => {
|
|
159
|
+
const result = await requestJson<{ data: string }>(
|
|
160
|
+
buildUrl('/api/export'),
|
|
161
|
+
{ method: 'POST', body: JSON.stringify({ password }) }
|
|
162
|
+
);
|
|
163
|
+
return result.data;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
importData: (encryptedData, password) => requestJson(buildUrl('/api/import'), {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
body: JSON.stringify({ encryptedData, password }),
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
writeClaudeConfig: () => requestJson(buildUrl('/api/write-config/claude'), { method: 'POST' }),
|
|
172
|
+
writeCodexConfig: () => requestJson(buildUrl('/api/write-config/codex'), { method: 'POST' }),
|
|
173
|
+
restoreClaudeConfig: () => requestJson(buildUrl('/api/restore-config/claude'), { method: 'POST' }),
|
|
174
|
+
restoreCodexConfig: () => requestJson(buildUrl('/api/restore-config/codex'), { method: 'POST' }),
|
|
175
|
+
checkClaudeBackup: () => requestJson(buildUrl('/api/check-backup/claude')),
|
|
176
|
+
checkCodexBackup: () => requestJson(buildUrl('/api/check-backup/codex')),
|
|
177
|
+
|
|
178
|
+
getStatistics: (days) => requestJson(buildUrl('/api/statistics', days ? { days } : undefined)),
|
|
179
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface JSONViewerProps {
|
|
4
|
+
data: string | Record<string, any> | any[];
|
|
5
|
+
title?: string;
|
|
6
|
+
collapsed?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const JSONViewer: React.FC<JSONViewerProps> = ({ data, title, collapsed = false }) => {
|
|
10
|
+
const [isExpanded, setIsExpanded] = useState(!collapsed);
|
|
11
|
+
const [copySuccess, setCopySuccess] = useState(false);
|
|
12
|
+
|
|
13
|
+
// 格式化 JSON 数据
|
|
14
|
+
const formatJSON = (input: string | Record<string, any> | any[]): string => {
|
|
15
|
+
try {
|
|
16
|
+
let jsonData: any;
|
|
17
|
+
if (typeof input === 'string') {
|
|
18
|
+
jsonData = JSON.parse(input);
|
|
19
|
+
} else {
|
|
20
|
+
jsonData = input;
|
|
21
|
+
}
|
|
22
|
+
return JSON.stringify(jsonData, null, 2);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return typeof input === 'string' ? input : String(input);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// 复制到剪贴板
|
|
29
|
+
const handleCopy = () => {
|
|
30
|
+
const formatted = formatJSON(data);
|
|
31
|
+
navigator.clipboard.writeText(formatted)
|
|
32
|
+
.then(() => {
|
|
33
|
+
setCopySuccess(true);
|
|
34
|
+
setTimeout(() => setCopySuccess(false), 2000);
|
|
35
|
+
})
|
|
36
|
+
.catch((error) => {
|
|
37
|
+
console.error('Failed to copy JSON:', error);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const formattedJSON = formatJSON(data);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="json-viewer">
|
|
45
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
|
46
|
+
{title && (
|
|
47
|
+
<h4 style={{ margin: 0, fontSize: '14px', color: 'var(--text-primary)' }}>
|
|
48
|
+
{title}
|
|
49
|
+
</h4>
|
|
50
|
+
)}
|
|
51
|
+
<div style={{ display: 'flex', gap: '8px' }}>
|
|
52
|
+
<button
|
|
53
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
54
|
+
className="btn btn-sm btn-secondary"
|
|
55
|
+
style={{ fontSize: '12px', padding: '2px 8px' }}
|
|
56
|
+
>
|
|
57
|
+
{isExpanded ? '折叠' : '展开'}
|
|
58
|
+
</button>
|
|
59
|
+
<button
|
|
60
|
+
onClick={handleCopy}
|
|
61
|
+
className="btn btn-sm btn-primary"
|
|
62
|
+
style={{ fontSize: '12px', padding: '2px 8px' }}
|
|
63
|
+
>
|
|
64
|
+
{copySuccess ? '已复制' : '复制'}
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
{isExpanded && (
|
|
69
|
+
<pre style={{
|
|
70
|
+
background: 'var(--bg-code)',
|
|
71
|
+
border: '1px solid var(--border-primary)',
|
|
72
|
+
borderRadius: '8px',
|
|
73
|
+
padding: '12px',
|
|
74
|
+
overflowX: 'auto',
|
|
75
|
+
fontSize: '12px',
|
|
76
|
+
lineHeight: '1.4',
|
|
77
|
+
color: 'var(--text-primary)',
|
|
78
|
+
maxHeight: '400px',
|
|
79
|
+
overflowY: 'auto',
|
|
80
|
+
margin: 0
|
|
81
|
+
}}>
|
|
82
|
+
<code>{formattedJSON}</code>
|
|
83
|
+
</pre>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export default JSONViewer;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#### 🚀 智谱 GLM Coding Plan
|
|
2
|
+
|
|
3
|
+
> 价格便宜,国内速度快,支持多工具(Claude Code等 20+ 大编程工具),而且其中的GLM-4.7是目前国内最强编程模型。适合对模型有一定要求,同时又无法获取国外模型的朋友入手。
|
|
4
|
+
>
|
|
5
|
+
> 链接:[https://www.bigmodel.cn/glm-coding](https://www.bigmodel.cn/glm-coding?ic=5AH7ATEZSC)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
#### 🗿 AI Code With
|
|
9
|
+
|
|
10
|
+
> 支持Claude、GPT、Gemni三大系列模型,可接入Claude Code、Codex,经实测,非常稳定,速度快,但是没有包月套餐,只能使用tokens流量,适合追求稳定的朋友。
|
|
11
|
+
>
|
|
12
|
+
> 链接:[https://aicodewith.com/](https://aicodewith.com/login?tab=register&invitation=QCA74W)
|
|
13
|
+
|
package/src/ui/main.tsx
ADDED