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.
Files changed (166) hide show
  1. package/.claude/commands/openspec/apply.md +23 -0
  2. package/.claude/commands/openspec/archive.md +27 -0
  3. package/.claude/commands/openspec/proposal.md +28 -0
  4. package/.claude/settings.local.json +12 -0
  5. package/.claude/skills/ui-ux-pro-max/SKILL.md +228 -0
  6. package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
  7. package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
  8. package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
  9. package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
  10. package/.claude/skills/ui-ux-pro-max/data/prompts.csv +24 -0
  11. package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  12. package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  13. package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  14. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  15. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  16. package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  17. package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  18. package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  19. package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  20. package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  21. package/.claude/skills/ui-ux-pro-max/data/styles.csv +59 -0
  22. package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
  23. package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  24. package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc +0 -0
  25. package/.claude/skills/ui-ux-pro-max/scripts/core.py +238 -0
  26. package/.claude/skills/ui-ux-pro-max/scripts/search.py +61 -0
  27. package/.cursor/commands/openspec-apply.md +23 -0
  28. package/.cursor/commands/openspec-archive.md +27 -0
  29. package/.cursor/commands/openspec-proposal.md +28 -0
  30. package/.cursor/commands/ui-ux-pro-max.md +226 -0
  31. package/.eslintrc.json +3 -0
  32. package/.shared/ui-ux-pro-max/data/charts.csv +26 -0
  33. package/.shared/ui-ux-pro-max/data/colors.csv +97 -0
  34. package/.shared/ui-ux-pro-max/data/landing.csv +31 -0
  35. package/.shared/ui-ux-pro-max/data/products.csv +97 -0
  36. package/.shared/ui-ux-pro-max/data/prompts.csv +24 -0
  37. package/.shared/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  38. package/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  39. package/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  40. package/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  41. package/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  42. package/.shared/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  43. package/.shared/ui-ux-pro-max/data/stacks/react.csv +54 -0
  44. package/.shared/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  45. package/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  46. package/.shared/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  47. package/.shared/ui-ux-pro-max/data/styles.csv +59 -0
  48. package/.shared/ui-ux-pro-max/data/typography.csv +58 -0
  49. package/.shared/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  50. package/.shared/ui-ux-pro-max/scripts/core.py +238 -0
  51. package/.shared/ui-ux-pro-max/scripts/search.py +61 -0
  52. package/AGENTS.md +18 -0
  53. package/CLAUDE.md +18 -0
  54. package/IMPLEMENTATION.md +157 -0
  55. package/LICENSE +21 -0
  56. package/README.md +165 -0
  57. package/dist/.next/types/app/api/config/route.js +52 -0
  58. package/dist/.next/types/app/api/gateway/[...path]/route.js +52 -0
  59. package/dist/.next/types/app/api/gateway/route.js +52 -0
  60. package/dist/.next/types/app/api/logs/route.js +52 -0
  61. package/dist/.next/types/app/api/models/route.js +52 -0
  62. package/dist/.next/types/app/api/providers/route.js +52 -0
  63. package/dist/.next/types/app/api/providers/test/route.js +52 -0
  64. package/dist/.next/types/app/api/service/start/route.js +52 -0
  65. package/dist/.next/types/app/api/service/status/route.js +52 -0
  66. package/dist/.next/types/app/api/service/stop/route.js +52 -0
  67. package/dist/.next/types/app/layout.js +22 -0
  68. package/dist/.next/types/app/logs/page.js +22 -0
  69. package/dist/.next/types/app/models/page.js +22 -0
  70. package/dist/.next/types/app/page.js +22 -0
  71. package/dist/.next/types/app/providers/page.js +22 -0
  72. package/dist/src/app/api/config/route.js +43 -0
  73. package/dist/src/app/api/gateway/[...path]/route.js +83 -0
  74. package/dist/src/app/api/gateway/route.js +63 -0
  75. package/dist/src/app/api/logs/route.js +34 -0
  76. package/dist/src/app/api/models/route.js +152 -0
  77. package/dist/src/app/api/providers/route.js +118 -0
  78. package/dist/src/app/api/providers/test/route.js +154 -0
  79. package/dist/src/app/api/service/start/route.js +55 -0
  80. package/dist/src/app/api/service/status/route.js +17 -0
  81. package/dist/src/app/api/service/stop/route.js +20 -0
  82. package/dist/src/app/components/ConfirmDialog.jsx +31 -0
  83. package/dist/src/app/components/Nav.jsx +45 -0
  84. package/dist/src/app/components/Toast.jsx +37 -0
  85. package/dist/src/app/components/ToastProvider.jsx +21 -0
  86. package/dist/src/app/layout.jsx +13 -0
  87. package/dist/src/app/logs/page.jsx +210 -0
  88. package/dist/src/app/models/page.jsx +291 -0
  89. package/dist/src/app/page.jsx +236 -0
  90. package/dist/src/app/providers/page.jsx +402 -0
  91. package/dist/src/cli/index.js +90 -0
  92. package/dist/src/db/database.js +69 -0
  93. package/dist/src/db/queries.js +261 -0
  94. package/dist/src/db/schema.js +67 -0
  95. package/dist/src/server/crypto.js +22 -0
  96. package/dist/src/server/gateway-server.js +200 -0
  97. package/dist/src/server/gateway.js +76 -0
  98. package/dist/src/server/logger.js +72 -0
  99. package/dist/src/server/providers/anthropic.js +52 -0
  100. package/dist/src/server/providers/gemini.js +64 -0
  101. package/dist/src/server/providers/index.js +16 -0
  102. package/dist/src/server/providers/openai.js +86 -0
  103. package/dist/src/server/providers/types.js +1 -0
  104. package/dist/src/server/service-manager.js +286 -0
  105. package/docs/TODO.md +19 -0
  106. package/next.config.js +7 -0
  107. package/openspec/AGENTS.md +456 -0
  108. package/openspec/changes/add-logging/proposal.md +18 -0
  109. package/openspec/changes/add-logging/specs/core/spec.md +21 -0
  110. package/openspec/changes/add-logging/tasks.md +16 -0
  111. package/openspec/changes/add-provider-test-connection/proposal.md +22 -0
  112. package/openspec/changes/add-provider-test-connection/specs/model-provider/spec.md +68 -0
  113. package/openspec/changes/add-provider-test-connection/tasks.md +31 -0
  114. package/openspec/changes/improve-gateway-startup/design.md +137 -0
  115. package/openspec/changes/improve-gateway-startup/proposal.md +33 -0
  116. package/openspec/changes/improve-gateway-startup/specs/api-gateway/spec.md +94 -0
  117. package/openspec/changes/improve-gateway-startup/specs/web-ui/spec.md +67 -0
  118. package/openspec/changes/improve-gateway-startup/tasks.md +47 -0
  119. package/openspec/changes/init-api-gateway/design.md +185 -0
  120. package/openspec/changes/init-api-gateway/proposal.md +30 -0
  121. package/openspec/changes/init-api-gateway/specs/api-gateway/spec.md +42 -0
  122. package/openspec/changes/init-api-gateway/specs/cli-tool/spec.md +40 -0
  123. package/openspec/changes/init-api-gateway/specs/model-management/spec.md +47 -0
  124. package/openspec/changes/init-api-gateway/specs/model-provider/spec.md +33 -0
  125. package/openspec/changes/init-api-gateway/specs/request-logging/spec.md +54 -0
  126. package/openspec/changes/init-api-gateway/specs/web-ui/spec.md +49 -0
  127. package/openspec/changes/init-api-gateway/tasks.md +84 -0
  128. package/openspec/project.md +58 -0
  129. package/package.json +51 -0
  130. package/postcss.config.js +6 -0
  131. package/src/app/api/config/route.ts +62 -0
  132. package/src/app/api/gateway/[...path]/route.ts +118 -0
  133. package/src/app/api/gateway/route.ts +77 -0
  134. package/src/app/api/logs/route.ts +48 -0
  135. package/src/app/api/models/route.ts +210 -0
  136. package/src/app/api/providers/route.ts +162 -0
  137. package/src/app/api/providers/test/route.ts +182 -0
  138. package/src/app/api/service/start/route.ts +73 -0
  139. package/src/app/api/service/status/route.ts +22 -0
  140. package/src/app/api/service/stop/route.ts +27 -0
  141. package/src/app/components/ConfirmDialog.tsx +63 -0
  142. package/src/app/components/Nav.tsx +66 -0
  143. package/src/app/components/Toast.tsx +61 -0
  144. package/src/app/components/ToastProvider.tsx +43 -0
  145. package/src/app/globals.css +71 -0
  146. package/src/app/layout.tsx +22 -0
  147. package/src/app/logs/page.tsx +261 -0
  148. package/src/app/models/page.tsx +500 -0
  149. package/src/app/page.tsx +742 -0
  150. package/src/app/providers/page.tsx +558 -0
  151. package/src/cli/index.ts +95 -0
  152. package/src/db/database.ts +125 -0
  153. package/src/db/queries.ts +339 -0
  154. package/src/db/schema.ts +117 -0
  155. package/src/server/crypto.ts +48 -0
  156. package/src/server/gateway-server.ts +306 -0
  157. package/src/server/gateway.ts +163 -0
  158. package/src/server/logger.ts +96 -0
  159. package/src/server/providers/anthropic.ts +121 -0
  160. package/src/server/providers/gemini.ts +112 -0
  161. package/src/server/providers/index.ts +20 -0
  162. package/src/server/providers/openai.ts +235 -0
  163. package/src/server/providers/types.ts +20 -0
  164. package/src/server/service-manager.ts +321 -0
  165. package/tailwind.config.js +16 -0
  166. package/tsconfig.json +29 -0
@@ -0,0 +1,558 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import Nav from '../components/Nav';
5
+ import { useToast } from '../components/ToastProvider';
6
+ import ConfirmDialog from '../components/ConfirmDialog';
7
+
8
+ interface Provider {
9
+ id: number;
10
+ name: string;
11
+ protocol: string;
12
+ base_url: string;
13
+ api_key: string;
14
+ }
15
+
16
+ interface Model {
17
+ id: number;
18
+ provider_id: number;
19
+ name: string;
20
+ model_id: string;
21
+ enabled: boolean;
22
+ }
23
+
24
+ export default function ProvidersPage() {
25
+ const [providers, setProviders] = useState<Provider[]>([]);
26
+ const [models, setModels] = useState<Model[]>([]);
27
+ const [loading, setLoading] = useState(true);
28
+ const [showModal, setShowModal] = useState(false);
29
+ const [editing, setEditing] = useState<Provider | null>(null);
30
+ const [showConfirm, setShowConfirm] = useState(false);
31
+ const [deleteId, setDeleteId] = useState<number | null>(null);
32
+ const [testingProviderId, setTestingProviderId] = useState<number | null>(null);
33
+ const [selectedModelId, setSelectedModelId] = useState<string>('');
34
+ const [testing, setTesting] = useState(false);
35
+ const [searchQuery, setSearchQuery] = useState('');
36
+ const [formData, setFormData] = useState({
37
+ name: '',
38
+ protocol: 'openai',
39
+ base_url: '',
40
+ api_key: '',
41
+ });
42
+ const [showApiKey, setShowApiKey] = useState(false);
43
+ const { showToast } = useToast();
44
+
45
+ useEffect(() => {
46
+ loadProviders();
47
+ loadModels();
48
+ }, []);
49
+
50
+ const loadProviders = async () => {
51
+ try {
52
+ const res = await fetch('/api/providers');
53
+ const data = await res.json();
54
+ setProviders(data);
55
+ } catch (error) {
56
+ console.error('Failed to load providers:', error);
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ };
61
+
62
+ const loadModels = async () => {
63
+ try {
64
+ const res = await fetch('/api/models');
65
+ const data = await res.json();
66
+ setModels(data);
67
+ } catch (error) {
68
+ console.error('Failed to load models:', error);
69
+ }
70
+ };
71
+
72
+ const loadProviderForEdit = async (id: number) => {
73
+ try {
74
+ const res = await fetch(`/api/providers?id=${id}&includeKey=true`);
75
+ if (!res.ok) {
76
+ throw new Error('Failed to load provider');
77
+ }
78
+ const data = await res.json();
79
+ return data;
80
+ } catch (error) {
81
+ console.error('Failed to load provider for edit:', error);
82
+ throw error;
83
+ }
84
+ };
85
+
86
+ const handleSubmit = async (e: React.FormEvent) => {
87
+ e.preventDefault();
88
+ try {
89
+ let response: Response;
90
+ if (editing) {
91
+ const updateData: any = {
92
+ id: editing.id,
93
+ name: formData.name,
94
+ protocol: formData.protocol,
95
+ base_url: formData.base_url,
96
+ };
97
+ if (formData.api_key && formData.api_key.trim() !== '') {
98
+ updateData.api_key = formData.api_key.trim();
99
+ }
100
+ response = await fetch('/api/providers', {
101
+ method: 'PUT',
102
+ headers: { 'Content-Type': 'application/json' },
103
+ body: JSON.stringify(updateData),
104
+ });
105
+ } else {
106
+ if (!formData.api_key || formData.api_key.trim() === '') {
107
+ showToast('API Key 不能为空', 'error');
108
+ return;
109
+ }
110
+ response = await fetch('/api/providers', {
111
+ method: 'POST',
112
+ headers: { 'Content-Type': 'application/json' },
113
+ body: JSON.stringify({
114
+ ...formData,
115
+ api_key: formData.api_key.trim(),
116
+ }),
117
+ });
118
+ }
119
+
120
+ if (!response.ok) {
121
+ const errorData = await response.json().catch(() => ({}));
122
+ const errorMessage = errorData.error || '保存失败';
123
+ showToast(errorMessage, 'error');
124
+ return;
125
+ }
126
+
127
+ setShowModal(false);
128
+ setEditing(null);
129
+ setFormData({ name: '', protocol: 'openai', base_url: '', api_key: '' });
130
+ setShowApiKey(false);
131
+ loadProviders();
132
+ showToast('供应商保存成功', 'success');
133
+ } catch (error: any) {
134
+ console.error('Failed to save provider:', error);
135
+ showToast('保存失败: ' + (error.message || '未知错误'), 'error');
136
+ }
137
+ };
138
+
139
+ const handleEdit = async (provider: Provider) => {
140
+ try {
141
+ // Load provider with API key for editing
142
+ const providerWithKey = await loadProviderForEdit(provider.id);
143
+ setEditing(provider);
144
+ setFormData({
145
+ name: providerWithKey.name,
146
+ protocol: providerWithKey.protocol,
147
+ base_url: providerWithKey.base_url,
148
+ api_key: providerWithKey.api_key || '', // Show existing key
149
+ });
150
+ setShowApiKey(false); // Start with hidden
151
+ setShowModal(true);
152
+ } catch (error) {
153
+ console.error('Failed to load provider for edit:', error);
154
+ showToast('加载供应商信息失败', 'error');
155
+ }
156
+ };
157
+
158
+ const handleDelete = (id: number) => {
159
+ setDeleteId(id);
160
+ setShowConfirm(true);
161
+ };
162
+
163
+ const confirmDelete = async () => {
164
+ if (deleteId) {
165
+ try {
166
+ await fetch(`/api/providers?id=${deleteId}`, { method: 'DELETE' });
167
+ loadProviders();
168
+ showToast('供应商已删除', 'success');
169
+ } catch (error) {
170
+ console.error('Failed to delete provider:', error);
171
+ showToast('删除失败', 'error');
172
+ }
173
+ }
174
+ setShowConfirm(false);
175
+ setDeleteId(null);
176
+ };
177
+
178
+ const handleTestConnection = (providerId: number) => {
179
+ const providerModels = models.filter(m => m.provider_id === providerId);
180
+ if (providerModels.length === 0) {
181
+ showToast('该供应商下没有模型,请先添加或拉取模型', 'error');
182
+ return;
183
+ }
184
+ setTestingProviderId(providerId);
185
+ setSelectedModelId('');
186
+ setSearchQuery('');
187
+ };
188
+
189
+ const startTest = async () => {
190
+ if (!testingProviderId || !selectedModelId) {
191
+ showToast('请选择要测试的模型', 'error');
192
+ return;
193
+ }
194
+
195
+ setTesting(true);
196
+ try {
197
+ const res = await fetch('/api/providers/test', {
198
+ method: 'POST',
199
+ headers: { 'Content-Type': 'application/json' },
200
+ body: JSON.stringify({
201
+ provider_id: testingProviderId,
202
+ model_id: parseInt(selectedModelId),
203
+ }),
204
+ });
205
+
206
+ const data = await res.json();
207
+ if (data.success) {
208
+ showToast(data.message || '连接成功,模型可用', 'success');
209
+ } else {
210
+ showToast(data.error || '连接失败', 'error');
211
+ }
212
+ } catch (error: any) {
213
+ console.error('Failed to test connection:', error);
214
+ showToast('测试连接失败: ' + (error.message || '未知错误'), 'error');
215
+ } finally {
216
+ setTesting(false);
217
+ setTestingProviderId(null);
218
+ setSelectedModelId('');
219
+ }
220
+ };
221
+
222
+ const cancelTest = () => {
223
+ setTestingProviderId(null);
224
+ setSelectedModelId('');
225
+ setSearchQuery('');
226
+ };
227
+
228
+ const getProviderModels = (providerId: number) => {
229
+ const providerModels = models.filter(m => m.provider_id === providerId);
230
+ if (!searchQuery.trim()) {
231
+ return providerModels;
232
+ }
233
+ const query = searchQuery.toLowerCase().trim();
234
+ return providerModels.filter(model =>
235
+ model.name.toLowerCase().includes(query) ||
236
+ model.model_id.toLowerCase().includes(query)
237
+ );
238
+ };
239
+
240
+ if (loading) {
241
+ return (
242
+ <>
243
+ <Nav />
244
+ <div className="min-h-screen flex items-center justify-center">
245
+ <div className="text-lg">加载中...</div>
246
+ </div>
247
+ </>
248
+ );
249
+ }
250
+
251
+ return (
252
+ <>
253
+ <Nav />
254
+ <main className="max-w-6xl mx-auto py-6 sm:px-6 lg:px-8">
255
+ <div className="px-4 py-4 sm:px-0">
256
+ <div className="flex justify-between items-center mb-5">
257
+ <div>
258
+ <h1 className="text-lg font-bold text-slate-800">供应商管理</h1>
259
+ <p className="text-xs text-slate-500 mt-1">管理 AI 模型供应商配置</p>
260
+ </div>
261
+ <button
262
+ onClick={() => {
263
+ setEditing(null);
264
+ setFormData({ name: '', protocol: 'openai', base_url: '', api_key: '' });
265
+ setShowApiKey(false);
266
+ setShowModal(true);
267
+ }}
268
+ className="inline-flex items-center px-3.5 py-1.5 border border-transparent text-xs font-semibold rounded-lg shadow-sm text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-emerald-500/50 transition-all duration-300"
269
+ >
270
+ <svg className="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
271
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
272
+ </svg>
273
+ 添加供应商
274
+ </button>
275
+ </div>
276
+
277
+ <div className="bg-white/70 backdrop-blur-sm shadow-md rounded-2xl border border-emerald-100/50 overflow-hidden">
278
+ <table className="min-w-full divide-y divide-slate-100">
279
+ <thead className="bg-emerald-50/30">
280
+ <tr>
281
+ <th className="px-4 py-2.5 text-left text-xs font-semibold text-slate-600 uppercase tracking-wider">
282
+ 名称
283
+ </th>
284
+ <th className="px-6 py-4 text-left text-xs font-semibold text-slate-600 uppercase tracking-wider">
285
+ 协议
286
+ </th>
287
+ <th className="px-6 py-4 text-left text-xs font-semibold text-slate-600 uppercase tracking-wider">
288
+ Base URL
289
+ </th>
290
+ <th className="px-6 py-3.5 text-right text-xs font-semibold text-gray-700 uppercase tracking-wider">
291
+ 操作
292
+ </th>
293
+ </tr>
294
+ </thead>
295
+ <tbody className="bg-white divide-y divide-slate-100">
296
+ {providers.length === 0 ? (
297
+ <tr>
298
+ <td colSpan={4} className="px-4 py-12 text-center">
299
+ <div className="text-slate-400">
300
+ <svg className="mx-auto h-10 w-10 mb-3 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
301
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
302
+ </svg>
303
+ <p className="text-xs text-slate-500">暂无供应商,请先添加供应商</p>
304
+ </div>
305
+ </td>
306
+ </tr>
307
+ ) : (
308
+ providers.map((provider) => (
309
+ <tr key={provider.id} className="hover:bg-emerald-50/20 transition-colors duration-300">
310
+ <td className="px-4 py-3 whitespace-nowrap">
311
+ <div className="text-xs font-medium text-slate-800">{provider.name}</div>
312
+ </td>
313
+ <td className="px-4 py-3 whitespace-nowrap">
314
+ <span className="px-2 py-0.5 inline-flex text-xs leading-4 font-semibold rounded-full bg-emerald-100/80 text-emerald-700 border border-emerald-200/50">
315
+ {provider.protocol}
316
+ </span>
317
+ </td>
318
+ <td className="px-4 py-3 whitespace-nowrap">
319
+ <div className="text-xs text-slate-600 font-mono">{provider.base_url}</div>
320
+ </td>
321
+ <td className="px-4 py-3 whitespace-nowrap text-right text-xs font-medium">
322
+ <div className="flex items-center justify-end space-x-3">
323
+ <button
324
+ onClick={() => handleTestConnection(provider.id)}
325
+ className="text-blue-600 hover:text-blue-700 font-medium transition-colors duration-300"
326
+ >
327
+ 测试连接
328
+ </button>
329
+ <button
330
+ onClick={() => handleEdit(provider)}
331
+ className="text-emerald-600 hover:text-emerald-700 font-medium transition-colors duration-300"
332
+ >
333
+ 编辑
334
+ </button>
335
+ <button
336
+ onClick={() => handleDelete(provider.id)}
337
+ className="text-rose-500 hover:text-rose-600 font-medium transition-colors duration-300"
338
+ >
339
+ 删除
340
+ </button>
341
+ </div>
342
+ </td>
343
+ </tr>
344
+ ))
345
+ )}
346
+ </tbody>
347
+ </table>
348
+ </div>
349
+ </div>
350
+ </main>
351
+
352
+ {showModal && (
353
+ <div className="fixed z-50 inset-0 overflow-y-auto">
354
+ <div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
355
+ <div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm transition-opacity" onClick={() => setShowModal(false)}></div>
356
+ <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-md sm:w-full border border-emerald-100/50">
357
+ <form onSubmit={handleSubmit} className="bg-white px-6 pt-5 pb-4 sm:p-6">
358
+ <div className="mb-5">
359
+ <h3 className="text-base font-bold text-slate-800 mb-1">
360
+ {editing ? '编辑供应商' : '添加供应商'}
361
+ </h3>
362
+ <p className="text-xs text-slate-500">配置 AI 模型供应商信息</p>
363
+ </div>
364
+ <div className="space-y-4">
365
+ <div>
366
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">名称</label>
367
+ <input
368
+ type="text"
369
+ required
370
+ value={formData.name}
371
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
372
+ autoComplete="off"
373
+ className="block w-full rounded-lg border border-slate-200 bg-white/80 px-3 py-2 text-xs shadow-sm transition-all duration-300 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-400/20 focus:ring-offset-1"
374
+ placeholder="例如: OpenAI"
375
+ />
376
+ </div>
377
+ <div>
378
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">协议</label>
379
+ <select
380
+ value={formData.protocol}
381
+ onChange={(e) => setFormData({ ...formData, protocol: e.target.value })}
382
+ autoComplete="off"
383
+ className="block w-full rounded-lg border border-slate-200 bg-white/80 px-3 py-2 text-xs shadow-sm transition-all duration-300 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-400/20 focus:ring-offset-1"
384
+ >
385
+ <option value="openai">OpenAI</option>
386
+ <option value="anthropic">Anthropic</option>
387
+ <option value="gemini">Gemini</option>
388
+ </select>
389
+ </div>
390
+ <div>
391
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">Base URL</label>
392
+ <input
393
+ type="url"
394
+ required
395
+ value={formData.base_url}
396
+ onChange={(e) => setFormData({ ...formData, base_url: e.target.value })}
397
+ autoComplete="off"
398
+ className="block w-full rounded-lg border border-slate-200 bg-white/80 px-3 py-2 text-xs shadow-sm transition-all duration-300 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-400/20 focus:ring-offset-1"
399
+ placeholder="https://api.openai.com/v1"
400
+ />
401
+ </div>
402
+ <div>
403
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">
404
+ API Key
405
+ {editing && (
406
+ <span className="ml-1 text-[10px] text-slate-400 font-normal">(已保存,输入新值可更新)</span>
407
+ )}
408
+ </label>
409
+ <div className="relative">
410
+ <input
411
+ type={showApiKey ? 'text' : 'password'}
412
+ required={!editing}
413
+ value={formData.api_key}
414
+ onChange={(e) => setFormData({ ...formData, api_key: e.target.value })}
415
+ autoComplete="new-password"
416
+ className="block w-full rounded-lg border border-slate-200 bg-white/80 px-3 py-2 pr-9 text-xs shadow-sm transition-all duration-300 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-400/20 focus:ring-offset-1"
417
+ placeholder={editing ? '留空则不更新,输入新值可更新 API Key' : '输入 API Key'}
418
+ />
419
+ <button
420
+ type="button"
421
+ onClick={() => setShowApiKey(!showApiKey)}
422
+ className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-slate-400 hover:text-slate-600 transition-colors"
423
+ tabIndex={-1}
424
+ >
425
+ {showApiKey ? (
426
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
427
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
428
+ </svg>
429
+ ) : (
430
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
431
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
432
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
433
+ </svg>
434
+ )}
435
+ </button>
436
+ </div>
437
+ </div>
438
+ </div>
439
+ <div className="mt-5 flex items-center justify-end space-x-2">
440
+ <button
441
+ type="button"
442
+ onClick={() => {
443
+ setShowModal(false);
444
+ setEditing(null);
445
+ }}
446
+ 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"
447
+ >
448
+ 取消
449
+ </button>
450
+ <button
451
+ type="submit"
452
+ className="px-4 py-1.5 border border-transparent rounded-lg text-xs font-semibold text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-emerald-500/50 transition-all duration-300"
453
+ >
454
+ 保存
455
+ </button>
456
+ </div>
457
+ </form>
458
+ </div>
459
+ </div>
460
+ </div>
461
+ )}
462
+
463
+ <ConfirmDialog
464
+ open={showConfirm}
465
+ title="确认删除"
466
+ message="确定要删除这个供应商吗?此操作不可恢复。"
467
+ onConfirm={confirmDelete}
468
+ onCancel={() => {
469
+ setShowConfirm(false);
470
+ setDeleteId(null);
471
+ }}
472
+ confirmText="删除"
473
+ cancelText="取消"
474
+ type="danger"
475
+ />
476
+
477
+ {/* Test Connection Modal */}
478
+ {testingProviderId && (
479
+ <div className="fixed z-50 inset-0 overflow-y-auto">
480
+ <div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
481
+ <div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm transition-opacity" onClick={cancelTest}></div>
482
+ <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-md sm:w-full border border-emerald-100/50">
483
+ <div className="bg-white px-6 pt-5 pb-4 sm:p-6">
484
+ <div className="mb-5">
485
+ <h3 className="text-base font-bold text-slate-800 mb-1">测试连接</h3>
486
+ <p className="text-xs text-slate-500">选择一个模型进行连接测试</p>
487
+ </div>
488
+ <div className="space-y-4">
489
+ <div>
490
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">搜索模型</label>
491
+ <div className="relative">
492
+ <input
493
+ type="text"
494
+ value={searchQuery}
495
+ onChange={(e) => setSearchQuery(e.target.value)}
496
+ disabled={testing}
497
+ autoComplete="off"
498
+ className="block w-full rounded-lg border border-slate-200 bg-white/80 pl-9 pr-3 py-2 text-xs shadow-sm transition-all duration-300 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-400/20 focus:ring-offset-1 disabled:opacity-50 disabled:cursor-not-allowed"
499
+ placeholder="输入模型名称或ID进行搜索..."
500
+ />
501
+ <svg className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
502
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
503
+ </svg>
504
+ </div>
505
+ </div>
506
+ <div>
507
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">选择模型</label>
508
+ <select
509
+ value={selectedModelId}
510
+ onChange={(e) => setSelectedModelId(e.target.value)}
511
+ disabled={testing}
512
+ className="block w-full rounded-lg border border-slate-200 bg-white/80 px-3 py-2 text-xs shadow-sm transition-all duration-300 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-400/20 focus:ring-offset-1 disabled:opacity-50 disabled:cursor-not-allowed"
513
+ >
514
+ <option value="">请选择模型</option>
515
+ {getProviderModels(testingProviderId).map((model) => (
516
+ <option key={model.id} value={model.id}>
517
+ {model.name} ({model.model_id})
518
+ </option>
519
+ ))}
520
+ </select>
521
+ </div>
522
+ </div>
523
+ <div className="mt-5 flex items-center justify-end space-x-2">
524
+ <button
525
+ type="button"
526
+ onClick={cancelTest}
527
+ disabled={testing}
528
+ 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 disabled:opacity-50 disabled:cursor-not-allowed"
529
+ >
530
+ 取消
531
+ </button>
532
+ <button
533
+ type="button"
534
+ onClick={startTest}
535
+ disabled={testing || !selectedModelId}
536
+ className="px-4 py-1.5 border border-transparent rounded-lg text-xs font-semibold text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-blue-500/50 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center"
537
+ >
538
+ {testing ? (
539
+ <>
540
+ <svg className="animate-spin -ml-1 mr-2 h-3 w-3 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
541
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
542
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
543
+ </svg>
544
+ 测试中...
545
+ </>
546
+ ) : (
547
+ '开始测试'
548
+ )}
549
+ </button>
550
+ </div>
551
+ </div>
552
+ </div>
553
+ </div>
554
+ </div>
555
+ )}
556
+ </>
557
+ );
558
+ }
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { GatewayServer } from '../server/gateway-server';
5
+ import { getDatabase } from '../db/database';
6
+ import { getConfig, setConfig } from '../db/queries';
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('aar')
12
+ .description('AI Agent Router - Unified gateway for managing multiple AI model providers')
13
+ .version('0.1.0');
14
+
15
+ program
16
+ .command('start')
17
+ .description('Start the API gateway server (gateway only, no Web UI)')
18
+ .option('-p, --port <port>', 'Port to listen on', '3000')
19
+ .option('--hostname <hostname>', 'Hostname to listen on', 'localhost')
20
+ .action(async (options) => {
21
+ const port = parseInt(options.port || '3000');
22
+ const hostname = options.hostname || 'localhost';
23
+
24
+ // Initialize database to get config
25
+ try {
26
+ getDatabase();
27
+ } catch (error: any) {
28
+ console.error('Failed to initialize database:', error);
29
+ process.exit(1);
30
+ }
31
+
32
+ // Get API key from config if configured
33
+ const apiKeyConfig = getConfig('api_key');
34
+ const apiKey = apiKeyConfig ? apiKeyConfig.value : undefined;
35
+
36
+ console.log(`Starting AI Agent Router Gateway Server`);
37
+ console.log(` Port: ${port}`);
38
+ console.log(` Hostname: ${hostname}`);
39
+ if (apiKey) {
40
+ console.log(` API Key: Configured (authentication enabled)`);
41
+ } else {
42
+ console.log(` API Key: Not configured (authentication disabled)`);
43
+ }
44
+
45
+ // Create and start gateway server
46
+ const server = new GatewayServer({
47
+ port,
48
+ hostname,
49
+ apiKey,
50
+ });
51
+
52
+ try {
53
+ await server.start();
54
+ } catch (error: any) {
55
+ console.error(`Failed to start gateway server: ${error.message}`);
56
+ process.exit(1);
57
+ }
58
+ });
59
+
60
+ program
61
+ .command('config')
62
+ .description('Manage gateway configuration')
63
+ .option('--get <key>', 'Get configuration value')
64
+ .option('--set <key> <value>', 'Set configuration value')
65
+ .action(async (options) => {
66
+ // Initialize database
67
+ try {
68
+ getDatabase();
69
+ } catch (error: any) {
70
+ console.error('Failed to initialize database:', error);
71
+ process.exit(1);
72
+ }
73
+
74
+ if (options.get) {
75
+ const config = getConfig(options.get);
76
+ if (config) {
77
+ console.log(config.value);
78
+ } else {
79
+ console.error(`Config key "${options.get}" not found`);
80
+ process.exit(1);
81
+ }
82
+ } else if (options.set) {
83
+ const [key, value] = options.set.split(' ');
84
+ if (!key || !value) {
85
+ console.error('Usage: --set <key> <value>');
86
+ process.exit(1);
87
+ }
88
+ setConfig(key, value);
89
+ console.log(`Config "${key}" set to "${value}"`);
90
+ } else {
91
+ program.help();
92
+ }
93
+ });
94
+
95
+ program.parse();