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,291 @@
1
+ 'use client';
2
+ import { useState, useEffect } from 'react';
3
+ import Nav from '../components/Nav';
4
+ import { useToast } from '../components/ToastProvider';
5
+ import ConfirmDialog from '../components/ConfirmDialog';
6
+ export default function ModelsPage() {
7
+ const [models, setModels] = useState([]);
8
+ const [providers, setProviders] = useState([]);
9
+ const [loading, setLoading] = useState(true);
10
+ const [showModal, setShowModal] = useState(false);
11
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
12
+ const [showFetchConfirm, setShowFetchConfirm] = useState(false);
13
+ const [deleteId, setDeleteId] = useState(null);
14
+ const [fetchProviderId, setFetchProviderId] = useState(null);
15
+ const [formData, setFormData] = useState({
16
+ provider_id: '',
17
+ name: '',
18
+ model_id: '',
19
+ enabled: true,
20
+ });
21
+ const { showToast } = useToast();
22
+ useEffect(() => {
23
+ loadData();
24
+ }, []);
25
+ const loadData = async () => {
26
+ try {
27
+ const [modelsRes, providersRes] = await Promise.all([
28
+ fetch('/api/models'),
29
+ fetch('/api/providers'),
30
+ ]);
31
+ const modelsData = await modelsRes.json();
32
+ const providersData = await providersRes.json();
33
+ setModels(modelsData);
34
+ setProviders(providersData);
35
+ }
36
+ catch (error) {
37
+ console.error('Failed to load data:', error);
38
+ }
39
+ finally {
40
+ setLoading(false);
41
+ }
42
+ };
43
+ const handleSubmit = async (e) => {
44
+ e.preventDefault();
45
+ try {
46
+ await fetch('/api/models', {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({
50
+ ...formData,
51
+ provider_id: parseInt(formData.provider_id),
52
+ }),
53
+ });
54
+ setShowModal(false);
55
+ setFormData({ provider_id: '', name: '', model_id: '', enabled: true });
56
+ loadData();
57
+ showToast('模型添加成功', 'success');
58
+ }
59
+ catch (error) {
60
+ console.error('Failed to save model:', error);
61
+ showToast('保存失败', 'error');
62
+ }
63
+ };
64
+ const handleToggleEnabled = async (id, enabled) => {
65
+ try {
66
+ await fetch('/api/models', {
67
+ method: 'PUT',
68
+ headers: { 'Content-Type': 'application/json' },
69
+ body: JSON.stringify({ id, enabled: !enabled }),
70
+ });
71
+ loadData();
72
+ }
73
+ catch (error) {
74
+ console.error('Failed to update model:', error);
75
+ }
76
+ };
77
+ const handleDelete = (id) => {
78
+ setDeleteId(id);
79
+ setShowDeleteConfirm(true);
80
+ };
81
+ const confirmDelete = async () => {
82
+ if (deleteId) {
83
+ try {
84
+ await fetch(`/api/models?id=${deleteId}`, { method: 'DELETE' });
85
+ loadData();
86
+ showToast('模型已删除', 'success');
87
+ }
88
+ catch (error) {
89
+ console.error('Failed to delete model:', error);
90
+ showToast('删除失败', 'error');
91
+ }
92
+ }
93
+ setShowDeleteConfirm(false);
94
+ setDeleteId(null);
95
+ };
96
+ const handleFetchModels = (providerId) => {
97
+ setFetchProviderId(providerId);
98
+ setShowFetchConfirm(true);
99
+ };
100
+ const confirmFetch = async () => {
101
+ if (fetchProviderId) {
102
+ try {
103
+ const res = await fetch('/api/models', {
104
+ method: 'PATCH',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({ provider_id: fetchProviderId }),
107
+ });
108
+ const data = await res.json();
109
+ showToast(`成功拉取 ${data.count} 个模型`, 'success');
110
+ loadData();
111
+ }
112
+ catch (error) {
113
+ console.error('Failed to fetch models:', error);
114
+ showToast('拉取失败: ' + error.message, 'error');
115
+ }
116
+ }
117
+ setShowFetchConfirm(false);
118
+ setFetchProviderId(null);
119
+ };
120
+ if (loading) {
121
+ return (<>
122
+ <Nav />
123
+ <div className="min-h-screen flex items-center justify-center">
124
+ <div className="text-lg">加载中...</div>
125
+ </div>
126
+ </>);
127
+ }
128
+ return (<>
129
+ <Nav />
130
+ <main className="max-w-6xl mx-auto py-6 sm:px-6 lg:px-8">
131
+ <div className="px-4 py-4 sm:px-0">
132
+ <div className="flex justify-between items-center mb-5">
133
+ <div>
134
+ <h1 className="text-lg font-bold text-slate-800">模型管理</h1>
135
+ <p className="text-xs text-slate-500 mt-1">管理 AI 模型配置和状态</p>
136
+ </div>
137
+ <button onClick={() => {
138
+ setFormData({ provider_id: '', name: '', model_id: '', enabled: true });
139
+ setShowModal(true);
140
+ }} 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">
141
+ <svg className="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
142
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4"/>
143
+ </svg>
144
+ 手动添加
145
+ </button>
146
+ </div>
147
+
148
+ {/* 一键拉取模型列表 - 移到列表上方 */}
149
+ <div className="mb-5 bg-white/70 backdrop-blur-sm shadow-md rounded-2xl border border-emerald-100/50 p-5">
150
+ <div className="flex items-center justify-between mb-4">
151
+ <div>
152
+ <h2 className="text-sm font-bold text-slate-800">快捷拉取</h2>
153
+ <p className="text-xs text-slate-500 mt-0.5">从供应商 API 自动拉取可用模型</p>
154
+ </div>
155
+ </div>
156
+ <div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
157
+ {providers.map((provider) => (<div key={provider.id} className="flex items-center justify-between p-3 border border-slate-200 rounded-xl hover:border-emerald-300 hover:shadow-sm transition-all duration-300 cursor-pointer bg-white/80 backdrop-blur-sm">
158
+ <div className="flex-1">
159
+ <div className="font-semibold text-xs text-slate-800">{provider.name}</div>
160
+ <div className="text-[10px] text-slate-500 mt-0.5">{provider.protocol}</div>
161
+ </div>
162
+ <button onClick={() => handleFetchModels(provider.id)} className="ml-3 inline-flex items-center px-2.5 py-1 border border-transparent text-[10px] font-semibold rounded-lg text-emerald-700 bg-emerald-50 hover:bg-emerald-100 transition-all duration-300">
163
+ <svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
164
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
165
+ </svg>
166
+ 拉取
167
+ </button>
168
+ </div>))}
169
+ </div>
170
+ </div>
171
+
172
+ <div className="bg-white/70 backdrop-blur-sm shadow-md rounded-2xl border border-emerald-100/50 overflow-hidden">
173
+ <table className="min-w-full divide-y divide-slate-100">
174
+ <thead className="bg-emerald-50/30">
175
+ <tr>
176
+ <th className="px-4 py-2.5 text-left text-xs font-semibold text-slate-600 uppercase tracking-wider">
177
+ 名称
178
+ </th>
179
+ <th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
180
+ 模型 ID
181
+ </th>
182
+ <th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
183
+ 供应商
184
+ </th>
185
+ <th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
186
+ 状态
187
+ </th>
188
+ <th className="px-6 py-3.5 text-right text-xs font-semibold text-gray-700 uppercase tracking-wider">
189
+ 操作
190
+ </th>
191
+ </tr>
192
+ </thead>
193
+ <tbody className="bg-white divide-y divide-slate-100">
194
+ {models.length === 0 ? (<tr>
195
+ <td colSpan={5} className="px-4 py-12 text-center">
196
+ <div className="text-slate-400">
197
+ <svg className="mx-auto h-10 w-10 mb-3 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
198
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
199
+ </svg>
200
+ <p className="text-xs text-slate-500">暂无模型,请先添加或拉取模型</p>
201
+ </div>
202
+ </td>
203
+ </tr>) : (models.map((model) => (<tr key={model.id} className="hover:bg-emerald-50/20 transition-colors duration-300">
204
+ <td className="px-4 py-3 whitespace-nowrap">
205
+ <div className="text-xs font-medium text-slate-800">{model.name}</div>
206
+ </td>
207
+ <td className="px-4 py-3 whitespace-nowrap">
208
+ <div className="text-xs text-slate-600 font-mono">{model.model_id}</div>
209
+ </td>
210
+ <td className="px-4 py-3 whitespace-nowrap">
211
+ <div className="text-xs text-slate-600">
212
+ <span className="font-medium">{model.provider_name}</span>
213
+ <span className="text-slate-400 ml-1">({model.provider_protocol})</span>
214
+ </div>
215
+ </td>
216
+ <td className="px-4 py-3 whitespace-nowrap">
217
+ <span className={`px-2 py-0.5 inline-flex text-xs leading-4 font-semibold rounded-full border ${model.enabled
218
+ ? 'bg-emerald-100/80 text-emerald-700 border-emerald-200/50'
219
+ : 'bg-slate-100/80 text-slate-600 border-slate-200/50'}`}>
220
+ {model.enabled ? '启用' : '禁用'}
221
+ </span>
222
+ </td>
223
+ <td className="px-4 py-3 whitespace-nowrap text-right text-xs font-medium">
224
+ <div className="flex items-center justify-end space-x-3">
225
+ <button onClick={() => handleToggleEnabled(model.id, model.enabled)} className="text-emerald-600 hover:text-emerald-700 font-medium transition-colors duration-300">
226
+ {model.enabled ? '禁用' : '启用'}
227
+ </button>
228
+ <button onClick={() => handleDelete(model.id)} className="text-rose-500 hover:text-rose-600 font-medium transition-colors duration-300">
229
+ 删除
230
+ </button>
231
+ </div>
232
+ </td>
233
+ </tr>)))}
234
+ </tbody>
235
+ </table>
236
+ </div>
237
+ </div>
238
+ </main>
239
+
240
+ {showModal && (<div className="fixed z-50 inset-0 overflow-y-auto">
241
+ <div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
242
+ <div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm transition-opacity" onClick={() => setShowModal(false)}></div>
243
+ <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">
244
+ <form onSubmit={handleSubmit} className="bg-white px-6 pt-5 pb-4 sm:p-6">
245
+ <div className="mb-5">
246
+ <h3 className="text-base font-bold text-slate-800 mb-1">添加模型</h3>
247
+ <p className="text-xs text-slate-500">手动添加新的 AI 模型</p>
248
+ </div>
249
+ <div className="space-y-4">
250
+ <div>
251
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">供应商</label>
252
+ <select required value={formData.provider_id} onChange={(e) => setFormData({ ...formData, provider_id: e.target.value })} autoComplete="off" 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">
253
+ <option value="">选择供应商</option>
254
+ {providers.map((p) => (<option key={p.id} value={p.id}>
255
+ {p.name}
256
+ </option>))}
257
+ </select>
258
+ </div>
259
+ <div>
260
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">名称</label>
261
+ <input type="text" required value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} autoComplete="off" 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" placeholder="例如: GPT-4"/>
262
+ </div>
263
+ <div>
264
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">模型 ID</label>
265
+ <input type="text" required value={formData.model_id} onChange={(e) => setFormData({ ...formData, model_id: e.target.value })} autoComplete="off" 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" placeholder="例如: gpt-4"/>
266
+ </div>
267
+ </div>
268
+ <div className="mt-5 flex items-center justify-end space-x-2">
269
+ <button type="button" onClick={() => setShowModal(false)} 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">
270
+ 取消
271
+ </button>
272
+ <button type="submit" 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">
273
+ 保存
274
+ </button>
275
+ </div>
276
+ </form>
277
+ </div>
278
+ </div>
279
+ </div>)}
280
+
281
+ <ConfirmDialog open={showDeleteConfirm} title="确认删除" message="确定要删除这个模型吗?此操作不可恢复。" onConfirm={confirmDelete} onCancel={() => {
282
+ setShowDeleteConfirm(false);
283
+ setDeleteId(null);
284
+ }} confirmText="删除" cancelText="取消" type="danger"/>
285
+
286
+ <ConfirmDialog open={showFetchConfirm} title="拉取模型列表" message="确定要从供应商拉取模型列表吗?" onConfirm={confirmFetch} onCancel={() => {
287
+ setShowFetchConfirm(false);
288
+ setFetchProviderId(null);
289
+ }} confirmText="拉取" cancelText="取消" type="info"/>
290
+ </>);
291
+ }
@@ -0,0 +1,236 @@
1
+ 'use client';
2
+ import { useState, useEffect } from 'react';
3
+ import Nav from './components/Nav';
4
+ import { useToast } from './components/ToastProvider';
5
+ export default function Home() {
6
+ const [config, setConfig] = useState({});
7
+ const [loading, setLoading] = useState(true);
8
+ const [saving, setSaving] = useState(false);
9
+ const [serviceStatus, setServiceStatus] = useState({ status: 'stopped' });
10
+ const [starting, setStarting] = useState(false);
11
+ const [stopping, setStopping] = useState(false);
12
+ const [showApiKey, setShowApiKey] = useState(false);
13
+ const { showToast } = useToast();
14
+ useEffect(() => {
15
+ loadConfig();
16
+ loadServiceStatus();
17
+ // Poll service status every 2 seconds
18
+ const interval = setInterval(loadServiceStatus, 2000);
19
+ return () => clearInterval(interval);
20
+ }, []);
21
+ const loadConfig = async () => {
22
+ try {
23
+ const res = await fetch('/api/config');
24
+ const data = await res.json();
25
+ setConfig({
26
+ port: data.port || '3000',
27
+ api_key: data.api_key || '',
28
+ });
29
+ }
30
+ catch (error) {
31
+ console.error('Failed to load config:', error);
32
+ }
33
+ finally {
34
+ setLoading(false);
35
+ }
36
+ };
37
+ const loadServiceStatus = async () => {
38
+ try {
39
+ const res = await fetch('/api/service/status');
40
+ const data = await res.json();
41
+ setServiceStatus(data);
42
+ }
43
+ catch (error) {
44
+ console.error('Failed to load service status:', error);
45
+ }
46
+ };
47
+ const handleStart = async () => {
48
+ if (starting || serviceStatus.status === 'running')
49
+ return;
50
+ setStarting(true);
51
+ try {
52
+ const port = config.port ? parseInt(config.port, 10) : undefined;
53
+ const res = await fetch('/api/service/start', {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify({ port }),
57
+ });
58
+ const data = await res.json();
59
+ if (data.error) {
60
+ showToast(`启动失败: ${data.error}`, 'error');
61
+ }
62
+ else {
63
+ showToast('服务已启动', 'success');
64
+ setServiceStatus(data);
65
+ }
66
+ }
67
+ catch (error) {
68
+ console.error('Failed to start service:', error);
69
+ showToast('启动失败', 'error');
70
+ }
71
+ finally {
72
+ setStarting(false);
73
+ // Reload status after a short delay
74
+ setTimeout(loadServiceStatus, 500);
75
+ }
76
+ };
77
+ const handleStop = async () => {
78
+ if (stopping || serviceStatus.status === 'stopped')
79
+ return;
80
+ setStopping(true);
81
+ try {
82
+ const res = await fetch('/api/service/stop', {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/json' },
85
+ });
86
+ const data = await res.json();
87
+ if (data.error) {
88
+ showToast(`停止失败: ${data.error}`, 'error');
89
+ }
90
+ else {
91
+ showToast('服务已停止', 'success');
92
+ setServiceStatus(data);
93
+ }
94
+ }
95
+ catch (error) {
96
+ console.error('Failed to stop service:', error);
97
+ showToast('停止失败', 'error');
98
+ }
99
+ finally {
100
+ setStopping(false);
101
+ // Reload status after a short delay
102
+ setTimeout(loadServiceStatus, 500);
103
+ }
104
+ };
105
+ const saveConfig = async () => {
106
+ setSaving(true);
107
+ try {
108
+ await fetch('/api/config', {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify({
112
+ key: 'port',
113
+ value: config.port,
114
+ }),
115
+ });
116
+ if (config.api_key) {
117
+ await fetch('/api/config', {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({
121
+ key: 'api_key',
122
+ value: config.api_key,
123
+ }),
124
+ });
125
+ }
126
+ showToast('配置已保存', 'success');
127
+ }
128
+ catch (error) {
129
+ console.error('Failed to save config:', error);
130
+ showToast('保存失败', 'error');
131
+ }
132
+ finally {
133
+ setSaving(false);
134
+ }
135
+ };
136
+ if (loading) {
137
+ return (<div className="min-h-screen flex items-center justify-center">
138
+ <div className="text-lg">加载中...</div>
139
+ </div>);
140
+ }
141
+ return (<div className="min-h-screen bg-gradient-to-br from-emerald-50/30 via-white to-slate-50/50">
142
+ <Nav />
143
+
144
+ <main className="max-w-3xl mx-auto py-6 sm:px-6 lg:px-8">
145
+ <div className="px-4 py-4 sm:px-0">
146
+ <div className="bg-white/70 backdrop-blur-sm shadow-md rounded-2xl border border-emerald-100/50">
147
+ <div className="px-6 py-5 sm:p-6">
148
+ <div className="mb-5">
149
+ <h2 className="text-lg font-bold text-slate-800 mb-1">网关配置</h2>
150
+ <p className="text-xs text-slate-500">配置 API 网关的运行参数</p>
151
+ </div>
152
+
153
+ <div className="space-y-3.5">
154
+ <div>
155
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">
156
+ 端口
157
+ </label>
158
+ <input type="number" value={config.port} onChange={(e) => setConfig({ ...config, port: e.target.value })} autoComplete="off" 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" placeholder="3000"/>
159
+ <p className="mt-1 text-xs text-slate-400">
160
+ 提示:如果 Web UI 运行在 3000 端口,建议将服务端口设置为 3001 或其他端口以避免冲突
161
+ </p>
162
+ </div>
163
+
164
+ <div>
165
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">
166
+ API Key (可选)
167
+ </label>
168
+ <div className="relative">
169
+ <input type={showApiKey ? 'text' : 'password'} value={config.api_key} onChange={(e) => setConfig({ ...config, api_key: e.target.value })} autoComplete="new-password" 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" placeholder="留空则不验证"/>
170
+ <button type="button" onClick={() => setShowApiKey(!showApiKey)} className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-slate-400 hover:text-slate-600 transition-colors" tabIndex={-1}>
171
+ {showApiKey ? (<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
172
+ <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"/>
173
+ </svg>) : (<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
174
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
175
+ <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"/>
176
+ </svg>)}
177
+ </button>
178
+ </div>
179
+ </div>
180
+
181
+ <div className="flex items-center justify-between pt-3">
182
+ <div>
183
+ <span className="text-xs text-slate-500">状态: </span>
184
+ <span className={`text-xs font-medium ${serviceStatus.status === 'running' ? 'text-emerald-600' : 'text-slate-500'}`}>
185
+ {serviceStatus.status === 'running' ? '运行中' : '已停止'}
186
+ </span>
187
+ {serviceStatus.status === 'running' && serviceStatus.port && (<span className="text-xs text-slate-400 ml-2">(端口: {serviceStatus.port})</span>)}
188
+ </div>
189
+ <div className="flex space-x-2">
190
+ <button onClick={saveConfig} disabled={saving || serviceStatus.status === 'running'} 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 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300">
191
+ {saving ? (<>
192
+ <svg className="animate-spin -ml-1 mr-1.5 h-3 w-3 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
193
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
194
+ <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>
195
+ </svg>
196
+ 保存中
197
+ </>) : ('保存配置')}
198
+ </button>
199
+ <button onClick={serviceStatus.status === 'running' ? handleStop : handleStart} disabled={starting || stopping} className={`inline-flex items-center px-3.5 py-1.5 border border-transparent text-xs font-semibold rounded-lg shadow-sm text-white focus:outline-none focus:ring-2 focus:ring-offset-1 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed ${serviceStatus.status === 'running'
200
+ ? 'bg-rose-500 hover:bg-rose-600 focus:ring-rose-500/50'
201
+ : 'bg-emerald-600 hover:bg-emerald-700 focus:ring-emerald-500/50'}`}>
202
+ {starting ? (<>
203
+ <svg className="animate-spin -ml-1 mr-1.5 h-3 w-3 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
204
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
205
+ <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>
206
+ </svg>
207
+ 启动中
208
+ </>) : stopping ? (<>
209
+ <svg className="animate-spin -ml-1 mr-1.5 h-3 w-3 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
210
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
211
+ <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>
212
+ </svg>
213
+ 停止中
214
+ </>) : serviceStatus.status === 'running' ? (<>
215
+ <svg className="w-3 h-3 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
216
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
217
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"/>
218
+ </svg>
219
+ 停止
220
+ </>) : (<>
221
+ <svg className="w-3 h-3 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
222
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
223
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
224
+ </svg>
225
+ 启动
226
+ </>)}
227
+ </button>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ </main>
235
+ </div>);
236
+ }