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,490 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import ReactMarkdown from 'react-markdown';
|
|
3
|
+
import { api } from '../api/client';
|
|
4
|
+
import type { Vendor, APIService, SourceType } from '../../types';
|
|
5
|
+
import recommendMd from '../docs/vendors-recommand.md?raw';
|
|
6
|
+
|
|
7
|
+
// TagInput 组件
|
|
8
|
+
function TagInput({ value = [], onChange, placeholder }: {
|
|
9
|
+
value: string[];
|
|
10
|
+
onChange: (tags: string[]) => void;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
}) {
|
|
13
|
+
const [inputValue, setInputValue] = useState('');
|
|
14
|
+
|
|
15
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
16
|
+
if (e.key === 'Enter' || e.key === ',') {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
const newTag = inputValue.trim();
|
|
19
|
+
if (newTag && !value.includes(newTag)) {
|
|
20
|
+
onChange([...value, newTag]);
|
|
21
|
+
setInputValue('');
|
|
22
|
+
}
|
|
23
|
+
} else if (e.key === 'Backspace' && !inputValue && value.length > 0) {
|
|
24
|
+
onChange(value.slice(0, -1));
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const removeTag = (indexToRemove: number) => {
|
|
29
|
+
onChange(value.filter((_, index) => index !== indexToRemove));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div style={{
|
|
34
|
+
border: `1px solid var(--border-primary)`,
|
|
35
|
+
borderRadius: '4px',
|
|
36
|
+
padding: '8px',
|
|
37
|
+
minHeight: '40px',
|
|
38
|
+
background: 'var(--bg-secondary)'
|
|
39
|
+
}}>
|
|
40
|
+
<div style={{
|
|
41
|
+
display: 'flex',
|
|
42
|
+
flexWrap: 'wrap',
|
|
43
|
+
gap: '4px',
|
|
44
|
+
alignItems: 'center'
|
|
45
|
+
}}>
|
|
46
|
+
{value.map((tag, index) => (
|
|
47
|
+
<span key={index} style={{
|
|
48
|
+
backgroundColor: 'var(--accent-light)',
|
|
49
|
+
color: 'var(--text-primary)',
|
|
50
|
+
padding: '4px 8px',
|
|
51
|
+
borderRadius: '4px',
|
|
52
|
+
fontSize: '12px',
|
|
53
|
+
display: 'flex',
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
gap: '4px'
|
|
56
|
+
}}>
|
|
57
|
+
{tag}
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
onClick={() => removeTag(index)}
|
|
61
|
+
style={{
|
|
62
|
+
background: 'none',
|
|
63
|
+
border: 'none',
|
|
64
|
+
color: 'var(--text-primary)',
|
|
65
|
+
cursor: 'pointer',
|
|
66
|
+
fontSize: '16px',
|
|
67
|
+
lineHeight: '1',
|
|
68
|
+
padding: '0',
|
|
69
|
+
marginLeft: '4px'
|
|
70
|
+
}}
|
|
71
|
+
onMouseOver={(e) => e.currentTarget.style.color = 'var(--accent-danger)'}
|
|
72
|
+
onMouseOut={(e) => e.currentTarget.style.color = 'var(--text-primary)'}
|
|
73
|
+
>
|
|
74
|
+
×
|
|
75
|
+
</button>
|
|
76
|
+
</span>
|
|
77
|
+
))}
|
|
78
|
+
<input
|
|
79
|
+
type="text"
|
|
80
|
+
value={inputValue}
|
|
81
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
82
|
+
onKeyDown={handleKeyDown}
|
|
83
|
+
placeholder={placeholder}
|
|
84
|
+
style={{
|
|
85
|
+
border: 'none',
|
|
86
|
+
outline: 'none',
|
|
87
|
+
flex: '1',
|
|
88
|
+
minWidth: '120px',
|
|
89
|
+
fontSize: '14px'
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const SOURCE_TYPE = {
|
|
98
|
+
'openai-chat': 'OpenAI Chat',
|
|
99
|
+
'openai-code': 'OpenAI Code',
|
|
100
|
+
'openai-responses': 'OpenAI Responses',
|
|
101
|
+
'claude-chat': 'Claude Chat',
|
|
102
|
+
'claude-code': 'Claude Code',
|
|
103
|
+
'deepseek-chat': 'DeepSeek Chat',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function VendorsPage() {
|
|
107
|
+
const [vendors, setVendors] = useState<Vendor[]>([]);
|
|
108
|
+
const [services, setServices] = useState<APIService[]>([]);
|
|
109
|
+
const [selectedVendor, setSelectedVendor] = useState<Vendor | null>(null);
|
|
110
|
+
const [showVendorModal, setShowVendorModal] = useState(false);
|
|
111
|
+
const [showServiceModal, setShowServiceModal] = useState(false);
|
|
112
|
+
const [showRecommendModal, setShowRecommendModal] = useState(false);
|
|
113
|
+
const [editingVendor, setEditingVendor] = useState<Vendor | null>(null);
|
|
114
|
+
const [editingService, setEditingService] = useState<APIService | null>(null);
|
|
115
|
+
const [supportedModels, setSupportedModels] = useState<string[]>([]);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
loadVendors();
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (selectedVendor) {
|
|
123
|
+
loadServices(selectedVendor.id);
|
|
124
|
+
}
|
|
125
|
+
}, [selectedVendor]);
|
|
126
|
+
|
|
127
|
+
const loadVendors = async () => {
|
|
128
|
+
const data = await api.getVendors();
|
|
129
|
+
setVendors(data);
|
|
130
|
+
if (data.length > 0 && !selectedVendor) {
|
|
131
|
+
setSelectedVendor(data[0]);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const loadServices = async (vendorId: string) => {
|
|
136
|
+
const data = await api.getAPIServices(vendorId);
|
|
137
|
+
setServices(data);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleCreateVendor = () => {
|
|
141
|
+
setEditingVendor(null);
|
|
142
|
+
setShowVendorModal(true);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const handleRecommend = () => {
|
|
146
|
+
setShowRecommendModal(true);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const handleEditVendor = (vendor: Vendor) => {
|
|
150
|
+
setEditingVendor(vendor);
|
|
151
|
+
setShowVendorModal(true);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleDeleteVendor = async (id: string) => {
|
|
155
|
+
if (confirm('确定要删除此供应商吗')) {
|
|
156
|
+
await api.deleteVendor(id);
|
|
157
|
+
loadVendors();
|
|
158
|
+
if (selectedVendor && selectedVendor.id === id) {
|
|
159
|
+
setSelectedVendor(null);
|
|
160
|
+
setServices([]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const handleSaveVendor = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
const formData = new FormData(e.currentTarget);
|
|
168
|
+
const vendor = {
|
|
169
|
+
name: formData.get('name') as string,
|
|
170
|
+
description: formData.get('description') as string,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (editingVendor) {
|
|
174
|
+
await api.updateVendor(editingVendor.id, vendor);
|
|
175
|
+
} else {
|
|
176
|
+
await api.createVendor(vendor);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
setShowVendorModal(false);
|
|
180
|
+
loadVendors();
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const handleCreateService = () => {
|
|
184
|
+
setEditingService(null);
|
|
185
|
+
setSupportedModels([]);
|
|
186
|
+
setShowServiceModal(true);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const handleEditService = (service: APIService) => {
|
|
190
|
+
setEditingService(service);
|
|
191
|
+
setSupportedModels(service.supportedModels || []);
|
|
192
|
+
setShowServiceModal(true);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
const handleDeleteService = async (id: string) => {
|
|
198
|
+
if (confirm('确定要删除此API服务吗')) {
|
|
199
|
+
await api.deleteAPIService(id);
|
|
200
|
+
if (selectedVendor) {
|
|
201
|
+
loadServices(selectedVendor.id);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const handleSaveService = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
const formData = new FormData(e.currentTarget);
|
|
209
|
+
|
|
210
|
+
const service = {
|
|
211
|
+
vendorId: selectedVendor!.id,
|
|
212
|
+
name: formData.get('name') as string,
|
|
213
|
+
apiUrl: formData.get('apiUrl') as string,
|
|
214
|
+
apiKey: formData.get('apiKey') as string,
|
|
215
|
+
timeout: parseInt(formData.get('timeout') as string) || 30000,
|
|
216
|
+
sourceType: formData.get('sourceType') as SourceType,
|
|
217
|
+
supportedModels: supportedModels.length > 0 ? supportedModels : undefined,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (editingService) {
|
|
221
|
+
await api.updateAPIService(editingService.id, service);
|
|
222
|
+
} else {
|
|
223
|
+
await api.createAPIService(service);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
setShowServiceModal(false);
|
|
227
|
+
setSupportedModels([]);
|
|
228
|
+
if (selectedVendor) {
|
|
229
|
+
loadServices(selectedVendor.id);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div>
|
|
235
|
+
<div className="page-header">
|
|
236
|
+
<h1>供应商管理</h1>
|
|
237
|
+
<p>管理API供应商和服务配置</p>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<div style={{ display: 'flex', gap: '20px' }}>
|
|
241
|
+
<div className="card" style={{ flex: '0 0 33%' }}>
|
|
242
|
+
<div className="toolbar">
|
|
243
|
+
<h3>供应商列表</h3>
|
|
244
|
+
<div style={{ display: 'flex', gap: '10px' }}>
|
|
245
|
+
<button
|
|
246
|
+
className="btn btn-secondary"
|
|
247
|
+
style={{
|
|
248
|
+
background: 'linear-gradient(135deg, #2563EB 0%, #F97316 100%)',
|
|
249
|
+
color: '#FFFFFF',
|
|
250
|
+
boxShadow: '0 4px 12px rgba(37, 99, 235, 0.3)',
|
|
251
|
+
border: 'none',
|
|
252
|
+
transition: 'all 0.3s ease',
|
|
253
|
+
cursor: 'pointer',
|
|
254
|
+
fontWeight: '600',
|
|
255
|
+
letterSpacing: '0.5px'
|
|
256
|
+
}}
|
|
257
|
+
onClick={handleRecommend}
|
|
258
|
+
onMouseEnter={(e) => {
|
|
259
|
+
e.currentTarget.style.transform = 'translateY(-2px)';
|
|
260
|
+
e.currentTarget.style.boxShadow = '0 8px 20px rgba(37, 99, 235, 0.4)';
|
|
261
|
+
}}
|
|
262
|
+
onMouseLeave={(e) => {
|
|
263
|
+
e.currentTarget.style.transform = 'translateY(0)';
|
|
264
|
+
e.currentTarget.style.boxShadow = '0 4px 12px rgba(37, 99, 235, 0.3)';
|
|
265
|
+
}}
|
|
266
|
+
onFocus={(e) => {
|
|
267
|
+
e.currentTarget.style.outline = '2px solid #2563EB';
|
|
268
|
+
e.currentTarget.style.outlineOffset = '2px';
|
|
269
|
+
}}
|
|
270
|
+
onBlur={(e) => {
|
|
271
|
+
e.currentTarget.style.outline = 'none';
|
|
272
|
+
}}
|
|
273
|
+
>
|
|
274
|
+
推荐
|
|
275
|
+
</button>
|
|
276
|
+
<button className="btn btn-primary" onClick={handleCreateVendor}>新增</button>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
{vendors.length === 0 ? (
|
|
280
|
+
<div className="empty-state"><p>暂无供应商</p></div>
|
|
281
|
+
) : (
|
|
282
|
+
<div style={{ marginTop: '10px' }}>
|
|
283
|
+
{vendors.map((vendor) => (
|
|
284
|
+
<div
|
|
285
|
+
key={vendor.id}
|
|
286
|
+
onClick={() => setSelectedVendor(vendor)}
|
|
287
|
+
style={{
|
|
288
|
+
padding: '12px',
|
|
289
|
+
marginBottom: '8px',
|
|
290
|
+
backgroundColor: selectedVendor && selectedVendor.id === vendor.id ? 'var(--accent-light)' : 'var(--bg-secondary)',
|
|
291
|
+
borderRadius: '4px',
|
|
292
|
+
cursor: 'pointer',
|
|
293
|
+
border: `1px solid var(--border-secondary)`,
|
|
294
|
+
color: 'var(--text-primary)'
|
|
295
|
+
}}
|
|
296
|
+
>
|
|
297
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
298
|
+
<div>
|
|
299
|
+
<div style={{ fontWeight: 500 }}>{vendor.name}</div>
|
|
300
|
+
{vendor.description && (
|
|
301
|
+
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '4px' }}>
|
|
302
|
+
{vendor.description}
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
<div className="action-buttons">
|
|
307
|
+
<button
|
|
308
|
+
className="btn btn-secondary"
|
|
309
|
+
style={{ padding: '4px 8px', fontSize: '12px' }}
|
|
310
|
+
onClick={(e) => {
|
|
311
|
+
e.stopPropagation();
|
|
312
|
+
handleEditVendor(vendor);
|
|
313
|
+
}}
|
|
314
|
+
>编辑</button>
|
|
315
|
+
<button
|
|
316
|
+
className="btn btn-danger"
|
|
317
|
+
style={{ padding: '4px 8px', fontSize: '12px' }}
|
|
318
|
+
onClick={(e) => {
|
|
319
|
+
e.stopPropagation();
|
|
320
|
+
handleDeleteVendor(vendor.id);
|
|
321
|
+
}}
|
|
322
|
+
>删除</button>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
))}
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div className="card" style={{ flex: 1 }}>
|
|
332
|
+
<div className="toolbar">
|
|
333
|
+
<h3>供应商API服务{selectedVendor && ` - ${selectedVendor.name}`}</h3>
|
|
334
|
+
{selectedVendor && (
|
|
335
|
+
<button className="btn btn-primary" onClick={handleCreateService}>新增服务</button>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
{!selectedVendor ? (
|
|
339
|
+
<div className="empty-state"><p>请先选择一个供应商</p></div>
|
|
340
|
+
) : services.length === 0 ? (
|
|
341
|
+
<div className="empty-state"><p>暂无API服务</p></div>
|
|
342
|
+
) : (
|
|
343
|
+
<table style={{ fontSize: 'smaller' }}>
|
|
344
|
+
<thead>
|
|
345
|
+
<tr>
|
|
346
|
+
<th style={{ whiteSpace: 'nowrap' }}>服务名称</th>
|
|
347
|
+
<th>源类型</th>
|
|
348
|
+
<th>API地址</th>
|
|
349
|
+
<th>模型列表</th>
|
|
350
|
+
<th>操作</th>
|
|
351
|
+
</tr>
|
|
352
|
+
</thead>
|
|
353
|
+
<tbody>
|
|
354
|
+
{services.map((service) => (
|
|
355
|
+
<tr key={service.id}>
|
|
356
|
+
<td>{service.name}</td>
|
|
357
|
+
<td>{service.sourceType ? SOURCE_TYPE[service.sourceType] : '-'}</td>
|
|
358
|
+
<td style={{ maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={service.apiUrl}>{service.apiUrl}</td>
|
|
359
|
+
<td>{service.supportedModels?.join(', ') || '-'}</td>
|
|
360
|
+
<td>
|
|
361
|
+
<div className="action-buttons">
|
|
362
|
+
<button className="btn btn-sm btn-secondary" onClick={() => handleEditService(service)}>编辑</button>
|
|
363
|
+
<button className="btn btn-sm btn-danger" onClick={() => handleDeleteService(service.id)}>删除</button>
|
|
364
|
+
</div>
|
|
365
|
+
</td>
|
|
366
|
+
</tr>
|
|
367
|
+
))}
|
|
368
|
+
</tbody>
|
|
369
|
+
</table>
|
|
370
|
+
)}
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
{showVendorModal && (
|
|
375
|
+
<div className="modal-overlay">
|
|
376
|
+
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
|
377
|
+
<div className="modal-header">
|
|
378
|
+
<h2>{editingVendor ? '编辑供应商' : '新增供应商'}</h2>
|
|
379
|
+
</div>
|
|
380
|
+
<form onSubmit={handleSaveVendor}>
|
|
381
|
+
<div className="form-group">
|
|
382
|
+
<label>供应商名称</label>
|
|
383
|
+
<input type="text" name="name" defaultValue={editingVendor ? editingVendor.name : ''} required />
|
|
384
|
+
</div>
|
|
385
|
+
<div className="form-group">
|
|
386
|
+
<label>描述</label>
|
|
387
|
+
<textarea name="description" rows={3} defaultValue={editingVendor ? editingVendor.description : ''} />
|
|
388
|
+
</div>
|
|
389
|
+
<div className="modal-footer">
|
|
390
|
+
<button type="button" className="btn btn-secondary" onClick={() => setShowVendorModal(false)}>取消</button>
|
|
391
|
+
<button type="submit" className="btn btn-primary">保存</button>
|
|
392
|
+
</div>
|
|
393
|
+
</form>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
397
|
+
|
|
398
|
+
{showServiceModal && (
|
|
399
|
+
<div className="modal-overlay">
|
|
400
|
+
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
|
401
|
+
<div className="modal-header">
|
|
402
|
+
<h2>{editingService ? '编辑供应商API服务' : '新增供应商API服务'}</h2>
|
|
403
|
+
</div>
|
|
404
|
+
<form onSubmit={handleSaveService}>
|
|
405
|
+
<div className="form-group">
|
|
406
|
+
<label>服务名称</label>
|
|
407
|
+
<input type="text" name="name" defaultValue={editingService ? editingService.name : ''} required />
|
|
408
|
+
</div>
|
|
409
|
+
<div className="form-group">
|
|
410
|
+
<label>源类型 <small>供应商接口返回的数据格式标准类型</small></label>
|
|
411
|
+
<select name="sourceType" defaultValue={editingService ? editingService.sourceType || '' : ''} required>
|
|
412
|
+
<option value="">请选择源类型</option>
|
|
413
|
+
<option value="openai-chat">OpenAI Chat</option>
|
|
414
|
+
<option value="openai-code">OpenAI Code</option>
|
|
415
|
+
<option value="openai-responses">OpenAI Responses</option>
|
|
416
|
+
<option value="claude-chat">Claude Chat</option>
|
|
417
|
+
<option value="claude-code">Claude Code</option>
|
|
418
|
+
<option value="deepseek-chat">DeepSeek Chat</option>
|
|
419
|
+
</select>
|
|
420
|
+
</div>
|
|
421
|
+
<div className="form-group">
|
|
422
|
+
<label>供应商API地址</label>
|
|
423
|
+
<input type="url" name="apiUrl" defaultValue={editingService ? editingService.apiUrl : ''} required />
|
|
424
|
+
</div>
|
|
425
|
+
<div className="form-group">
|
|
426
|
+
<label>供应商API密钥</label>
|
|
427
|
+
<input type="password" name="apiKey" defaultValue={editingService ? editingService.apiKey : ''} required />
|
|
428
|
+
</div>
|
|
429
|
+
<div className="form-group">
|
|
430
|
+
<label>超时时间(ms)</label>
|
|
431
|
+
<input type="number" name="timeout" defaultValue={editingService ? editingService.timeout : 30000} />
|
|
432
|
+
</div>
|
|
433
|
+
<div className="form-group">
|
|
434
|
+
<label>支持的模型列表</label>
|
|
435
|
+
<TagInput
|
|
436
|
+
key={editingService?.id || 'new'}
|
|
437
|
+
value={supportedModels}
|
|
438
|
+
onChange={setSupportedModels}
|
|
439
|
+
placeholder="输入模型名,按Enter或逗号添加"
|
|
440
|
+
/>
|
|
441
|
+
</div>
|
|
442
|
+
<div className="modal-footer">
|
|
443
|
+
<button type="button" className="btn btn-secondary" onClick={() => setShowServiceModal(false)}>取消</button>
|
|
444
|
+
<button type="submit" className="btn btn-primary">保存</button>
|
|
445
|
+
</div>
|
|
446
|
+
</form>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
)}
|
|
450
|
+
|
|
451
|
+
{showRecommendModal && (
|
|
452
|
+
<div className="modal-overlay">
|
|
453
|
+
<div className="modal" style={{ maxWidth: '800px' }} onClick={(e) => e.stopPropagation()}>
|
|
454
|
+
<div className="modal-header">
|
|
455
|
+
<h2>供应商推荐</h2>
|
|
456
|
+
</div>
|
|
457
|
+
<div className="modal-body">
|
|
458
|
+
<div className="markdown-content">
|
|
459
|
+
<ReactMarkdown
|
|
460
|
+
components={{
|
|
461
|
+
a: ({ href, children }) => (
|
|
462
|
+
<a
|
|
463
|
+
href={href}
|
|
464
|
+
style={{
|
|
465
|
+
color: '#2563EB',
|
|
466
|
+
borderBottom: 'solid 1px #2563EB'
|
|
467
|
+
}}
|
|
468
|
+
target="_blank"
|
|
469
|
+
rel="noopener noreferrer"
|
|
470
|
+
>
|
|
471
|
+
{children}
|
|
472
|
+
</a>
|
|
473
|
+
)
|
|
474
|
+
}}
|
|
475
|
+
>
|
|
476
|
+
{recommendMd}
|
|
477
|
+
</ReactMarkdown>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
<div className="modal-footer">
|
|
481
|
+
<button type="button" className="btn btn-secondary" onClick={() => setShowRecommendModal(false)}>关闭</button>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
</div>
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export default VendorsPage;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { api } from '../api/client';
|
|
3
|
+
|
|
4
|
+
type ConfigType = 'claude' | 'codex';
|
|
5
|
+
|
|
6
|
+
function WriteConfigPage() {
|
|
7
|
+
const [isWriting, setIsWriting] = useState<{[key in ConfigType]: boolean}>({
|
|
8
|
+
claude: false,
|
|
9
|
+
codex: false
|
|
10
|
+
});
|
|
11
|
+
const [isRestoring, setIsRestoring] = useState<{[key in ConfigType]: boolean}>({
|
|
12
|
+
claude: false,
|
|
13
|
+
codex: false
|
|
14
|
+
});
|
|
15
|
+
const [hasBackup, setHasBackup] = useState<{[key in ConfigType]: boolean}>({
|
|
16
|
+
claude: false,
|
|
17
|
+
codex: false
|
|
18
|
+
});
|
|
19
|
+
const [message, setMessage] = useState('');
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
checkBackups();
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const checkBackups = async () => {
|
|
26
|
+
try {
|
|
27
|
+
const [claudeBackup, codexBackup] = await Promise.all([
|
|
28
|
+
api.checkClaudeBackup(),
|
|
29
|
+
api.checkCodexBackup()
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
setHasBackup({
|
|
33
|
+
claude: claudeBackup.exists,
|
|
34
|
+
codex: codexBackup.exists
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Failed to check backups:', error);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleWriteConfig = async (type: ConfigType) => {
|
|
42
|
+
setIsWriting(prev => ({ ...prev, [type]: true }));
|
|
43
|
+
setMessage('');
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const result = type === 'claude'
|
|
47
|
+
? await api.writeClaudeConfig()
|
|
48
|
+
: await api.writeCodexConfig();
|
|
49
|
+
|
|
50
|
+
if (result) {
|
|
51
|
+
setMessage(`${type === 'claude' ? 'Claude Code' : 'Codex'}配置文件写入成功!原始文件已备份为 .bak 文件。`);
|
|
52
|
+
await checkBackups(); // 重新检查备份状态
|
|
53
|
+
} else {
|
|
54
|
+
setMessage(`写入失败: 操作未成功`);
|
|
55
|
+
}
|
|
56
|
+
} catch (error: any) {
|
|
57
|
+
setMessage(`写入失败: ${error.message}`);
|
|
58
|
+
} finally {
|
|
59
|
+
setIsWriting(prev => ({ ...prev, [type]: false }));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleRestoreConfig = async (type: ConfigType) => {
|
|
64
|
+
setIsRestoring(prev => ({ ...prev, [type]: true }));
|
|
65
|
+
setMessage('');
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = type === 'claude'
|
|
69
|
+
? await api.restoreClaudeConfig()
|
|
70
|
+
: await api.restoreCodexConfig();
|
|
71
|
+
|
|
72
|
+
if (result) {
|
|
73
|
+
setMessage(`${type === 'claude' ? 'Claude Code' : 'Codex'}配置文件已从备份恢复!`);
|
|
74
|
+
await checkBackups(); // 重新检查备份状态
|
|
75
|
+
} else {
|
|
76
|
+
setMessage(`恢复失败: 操作未成功`);
|
|
77
|
+
}
|
|
78
|
+
} catch (error: any) {
|
|
79
|
+
setMessage(`恢复失败: ${error.message}`);
|
|
80
|
+
} finally {
|
|
81
|
+
setIsRestoring(prev => ({ ...prev, [type]: false }));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div>
|
|
87
|
+
<div className="page-header">
|
|
88
|
+
<h1>写入配置</h1>
|
|
89
|
+
<p>将Claude Code和Codex的配置文件写入到用户目录</p>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div className="card" style={{ marginBottom: '20px' }}>
|
|
93
|
+
<h3>Claude Code配置</h3>
|
|
94
|
+
<p style={{ color: '#7f8c8d', marginBottom: '15px' }}>
|
|
95
|
+
为Claude Code工具写入配置文件。原始文件将被备份为.bak文件。
|
|
96
|
+
</p>
|
|
97
|
+
|
|
98
|
+
<div style={{ marginBottom: '15px' }}>
|
|
99
|
+
<h4>配置文件:</h4>
|
|
100
|
+
<ul style={{ color: '#7f8c8d', lineHeight: '1.6' }}>
|
|
101
|
+
<li><code>~/.claude/settings.json</code> - Claude Code设置</li>
|
|
102
|
+
<li><code>~/.claude.json</code> - Claude Code初始化设置</li>
|
|
103
|
+
</ul>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div style={{ display: 'flex', gap: '10px' }}>
|
|
107
|
+
<button
|
|
108
|
+
className="btn btn-primary"
|
|
109
|
+
onClick={() => handleWriteConfig('claude')}
|
|
110
|
+
disabled={isWriting.claude || hasBackup.claude}
|
|
111
|
+
>
|
|
112
|
+
{isWriting.claude ? '写入中...' : '写入Claude Code配置'}
|
|
113
|
+
</button>
|
|
114
|
+
|
|
115
|
+
<button
|
|
116
|
+
className="btn btn-secondary"
|
|
117
|
+
onClick={() => handleRestoreConfig('claude')}
|
|
118
|
+
disabled={isRestoring.claude || !hasBackup.claude}
|
|
119
|
+
>
|
|
120
|
+
{isRestoring.claude ? '恢复中...' : '恢复Claude Code配置'}
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{!hasBackup.claude && (
|
|
125
|
+
<p style={{ color: '#95a5a6', fontSize: '12px', marginTop: '10px' }}>
|
|
126
|
+
没有找到备份文件,恢复功能不可用
|
|
127
|
+
</p>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{hasBackup.claude && (
|
|
131
|
+
<p style={{ color: '#e74c3c', fontSize: '12px', marginTop: '10px' }}>
|
|
132
|
+
⚠️ 备份文件已存在,为避免覆盖原始备份,请先恢复或手动删除备份文件后再写入
|
|
133
|
+
</p>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div className="card">
|
|
138
|
+
<h3>Codex配置</h3>
|
|
139
|
+
<p style={{ color: '#7f8c8d', marginBottom: '15px' }}>
|
|
140
|
+
为Codex工具写入配置文件。原始文件将被备份为.bak文件。
|
|
141
|
+
</p>
|
|
142
|
+
|
|
143
|
+
<div style={{ marginBottom: '15px' }}>
|
|
144
|
+
<h4>配置文件:</h4>
|
|
145
|
+
<ul style={{ color: '#7f8c8d', lineHeight: '1.6' }}>
|
|
146
|
+
<li><code>~/.codex/config.toml</code> - Codex配置</li>
|
|
147
|
+
<li><code>~/.codex/auth.json</code> - Codex认证信息</li>
|
|
148
|
+
</ul>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div style={{ display: 'flex', gap: '10px' }}>
|
|
152
|
+
<button
|
|
153
|
+
className="btn btn-primary"
|
|
154
|
+
onClick={() => handleWriteConfig('codex')}
|
|
155
|
+
disabled={isWriting.codex || hasBackup.codex}
|
|
156
|
+
>
|
|
157
|
+
{isWriting.codex ? '写入中...' : '写入Codex配置'}
|
|
158
|
+
</button>
|
|
159
|
+
|
|
160
|
+
<button
|
|
161
|
+
className="btn btn-secondary"
|
|
162
|
+
onClick={() => handleRestoreConfig('codex')}
|
|
163
|
+
disabled={isRestoring.codex || !hasBackup.codex}
|
|
164
|
+
>
|
|
165
|
+
{isRestoring.codex ? '恢复中...' : '恢复Codex配置'}
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{!hasBackup.codex && (
|
|
170
|
+
<p style={{ color: '#95a5a6', fontSize: '12px', marginTop: '10px' }}>
|
|
171
|
+
没有找到备份文件,恢复功能不可用
|
|
172
|
+
</p>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{hasBackup.codex && (
|
|
176
|
+
<p style={{ color: '#e74c3c', fontSize: '12px', marginTop: '10px' }}>
|
|
177
|
+
⚠️ 备份文件已存在,为避免覆盖原始备份,请先恢复或手动删除备份文件后再写入
|
|
178
|
+
</p>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{message && (
|
|
183
|
+
<div style={{
|
|
184
|
+
marginTop: '20px',
|
|
185
|
+
padding: '10px',
|
|
186
|
+
borderRadius: '4px',
|
|
187
|
+
backgroundColor: message.includes('成功') ? '#d4edda' : '#f8d7da',
|
|
188
|
+
color: message.includes('成功') ? '#155724' : '#721c24',
|
|
189
|
+
border: `1px solid ${message.includes('成功') ? '#c3e6cb' : '#f5c6cb'}`
|
|
190
|
+
}}>
|
|
191
|
+
{message}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default WriteConfigPage;
|