@allbluecn/admin-app 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 (276) hide show
  1. package/package.json +86 -0
  2. package/src/client.tsx +31 -0
  3. package/src/components/admin/admin-user-menu.tsx +90 -0
  4. package/src/components/admin/developer-log.tsx +356 -0
  5. package/src/components/ai-provider/model-list-editor.tsx +147 -0
  6. package/src/components/ai-provider/provider-form.tsx +434 -0
  7. package/src/components/ai-provider/provider-icon.tsx +43 -0
  8. package/src/components/ai-provider/provider-logo.tsx +87 -0
  9. package/src/components/ai-provider/provider-table.tsx +258 -0
  10. package/src/components/auth/admin-login-form.tsx +214 -0
  11. package/src/components/auth/auth-brand-panel.tsx +60 -0
  12. package/src/components/auth/auth-header.tsx +38 -0
  13. package/src/components/auth/auth-layout.tsx +66 -0
  14. package/src/components/auth/auth-providers.tsx +34 -0
  15. package/src/components/auth/icon-input.tsx +46 -0
  16. package/src/components/auth/oauth-section.tsx +51 -0
  17. package/src/components/auth/password-input.tsx +67 -0
  18. package/src/components/common/page-header.tsx +42 -0
  19. package/src/components/common/placeholder-page.tsx +42 -0
  20. package/src/components/component/component-form-dialog.tsx +348 -0
  21. package/src/components/component/component-import-dialog.tsx +235 -0
  22. package/src/components/component/component-table.tsx +153 -0
  23. package/src/components/data-table/data-table-pagination.tsx +114 -0
  24. package/src/components/data-table/data-table.tsx +218 -0
  25. package/src/components/data-table/table-detail-layout.tsx +99 -0
  26. package/src/components/dependency/changelog-dialog.tsx +108 -0
  27. package/src/components/dependency/columns.tsx +232 -0
  28. package/src/components/dependency/dependency-table.tsx +368 -0
  29. package/src/components/dependency/edit-dialog.tsx +200 -0
  30. package/src/components/knowledge-public/bookmark-form-dialog.tsx +215 -0
  31. package/src/components/knowledge-public/compliance-form-dialog.tsx +308 -0
  32. package/src/components/knowledge-public/legal-form-dialog.tsx +315 -0
  33. package/src/components/layout/sidebar-02/app-sidebar.tsx +160 -0
  34. package/src/components/layout/sidebar-02/index.tsx +33 -0
  35. package/src/components/layout/sidebar-02/logo.tsx +40 -0
  36. package/src/components/layout/sidebar-02/nav-main.tsx +153 -0
  37. package/src/components/layout/sidebar-02/nav-notifications.tsx +78 -0
  38. package/src/components/layout/sidebar-02/team-switcher.tsx +106 -0
  39. package/src/components/magicui/animated-theme-toggler.tsx +272 -0
  40. package/src/components/magicui/rainbow-button.tsx +78 -0
  41. package/src/components/market/columns.tsx +151 -0
  42. package/src/components/market/market-app-form.tsx +237 -0
  43. package/src/components/market/market-app-list.tsx +156 -0
  44. package/src/components/monitoring/error-list.tsx +143 -0
  45. package/src/components/monitoring/health-cards.tsx +152 -0
  46. package/src/components/monitoring/metrics-stats.tsx +184 -0
  47. package/src/components/monitoring/request-log-table.tsx +179 -0
  48. package/src/components/providers/theme-toggle.tsx +59 -0
  49. package/src/components/settings/avatar/avatar-config.ts +46 -0
  50. package/src/components/ui/alert-dialog.tsx +216 -0
  51. package/src/components/ui/alert.tsx +76 -0
  52. package/src/components/ui/avatar.tsx +129 -0
  53. package/src/components/ui/badge.tsx +66 -0
  54. package/src/components/ui/button.tsx +84 -0
  55. package/src/components/ui/card.tsx +120 -0
  56. package/src/components/ui/checkbox.tsx +50 -0
  57. package/src/components/ui/collapsible.tsx +50 -0
  58. package/src/components/ui/dialog.tsx +185 -0
  59. package/src/components/ui/dropdown-menu.tsx +286 -0
  60. package/src/components/ui/field.tsx +255 -0
  61. package/src/components/ui/input-group.tsx +171 -0
  62. package/src/components/ui/input.tsx +36 -0
  63. package/src/components/ui/label.tsx +41 -0
  64. package/src/components/ui/pagination.tsx +146 -0
  65. package/src/components/ui/select.tsx +209 -0
  66. package/src/components/ui/separator.tsx +45 -0
  67. package/src/components/ui/sheet.tsx +164 -0
  68. package/src/components/ui/sidebar.tsx +719 -0
  69. package/src/components/ui/skeleton.tsx +30 -0
  70. package/src/components/ui/sonner.tsx +37 -0
  71. package/src/components/ui/switch.tsx +48 -0
  72. package/src/components/ui/table.tsx +133 -0
  73. package/src/components/ui/textarea.tsx +35 -0
  74. package/src/components/ui/tooltip.tsx +74 -0
  75. package/src/components/user/columns.tsx +153 -0
  76. package/src/components/user/settings-activity-log.tsx +358 -0
  77. package/src/components/user/settings-profile/constants.ts +42 -0
  78. package/src/components/user/settings-profile/types.ts +47 -0
  79. package/src/components/user/settings-profile.tsx +664 -0
  80. package/src/components/user/user-create-dialog.tsx +137 -0
  81. package/src/components/user/user-filters.tsx +117 -0
  82. package/src/hooks/use-mobile.ts +36 -0
  83. package/src/instrument.client.ts +34 -0
  84. package/src/lib/anyhub/capabilities.ts +59 -0
  85. package/src/lib/anyhub/types.ts +43 -0
  86. package/src/lib/auth-client.ts +83 -0
  87. package/src/lib/auth-utils.ts +24 -0
  88. package/src/lib/auth.config.ts +264 -0
  89. package/src/lib/avatar.ts +41 -0
  90. package/src/lib/i18n/client.ts +32 -0
  91. package/src/lib/i18n/config.ts +40 -0
  92. package/src/lib/i18n/detect.ts +43 -0
  93. package/src/lib/i18n/locales/en/admin-ai.json +128 -0
  94. package/src/lib/i18n/locales/en/admin-billing.json +56 -0
  95. package/src/lib/i18n/locales/en/admin-users.json +88 -0
  96. package/src/lib/i18n/locales/en/common.json +877 -0
  97. package/src/lib/i18n/locales/en/enterprise.json +208 -0
  98. package/src/lib/i18n/locales/zh/admin-ai.json +128 -0
  99. package/src/lib/i18n/locales/zh/admin-billing.json +56 -0
  100. package/src/lib/i18n/locales/zh/admin-users.json +88 -0
  101. package/src/lib/i18n/locales/zh/common.json +877 -0
  102. package/src/lib/i18n/locales/zh/enterprise.json +208 -0
  103. package/src/lib/i18n/server.ts +18 -0
  104. package/src/lib/i18n/types.ts +5 -0
  105. package/src/lib/monitoring-benchmarks.ts +85 -0
  106. package/src/lib/monitoring-descriptions.ts +78 -0
  107. package/src/lib/prisma.ts +18 -0
  108. package/src/lib/router-context.ts +38 -0
  109. package/src/lib/session.ts +38 -0
  110. package/src/lib/utils.ts +23 -0
  111. package/src/providers/index.tsx +45 -0
  112. package/src/routeTree.gen.ts +1199 -0
  113. package/src/router.tsx +51 -0
  114. package/src/routes/__root.tsx +170 -0
  115. package/src/routes/_nav/ai-providers/$id/edit/route.lazy.tsx +74 -0
  116. package/src/routes/_nav/ai-providers/$id/edit/route.tsx +35 -0
  117. package/src/routes/_nav/ai-providers/index/route.lazy.tsx +33 -0
  118. package/src/routes/_nav/ai-providers/new/route.lazy.tsx +31 -0
  119. package/src/routes/_nav/ai-providers/new/route.tsx +20 -0
  120. package/src/routes/_nav/ai-providers/route.lazy.tsx +26 -0
  121. package/src/routes/_nav/ai-providers/route.tsx +31 -0
  122. package/src/routes/_nav/apps/route.lazy.tsx +30 -0
  123. package/src/routes/_nav/apps/route.tsx +20 -0
  124. package/src/routes/_nav/audit/route.lazy.tsx +100 -0
  125. package/src/routes/_nav/audit/route.tsx +25 -0
  126. package/src/routes/_nav/billing/invoices/route.lazy.tsx +86 -0
  127. package/src/routes/_nav/billing/invoices/route.tsx +25 -0
  128. package/src/routes/_nav/billing/route.lazy.tsx +48 -0
  129. package/src/routes/_nav/billing/route.tsx +27 -0
  130. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +160 -0
  131. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.tsx +25 -0
  132. package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +113 -0
  133. package/src/routes/_nav/billing/subscriptions/route.tsx +28 -0
  134. package/src/routes/_nav/compliance/route.lazy.tsx +184 -0
  135. package/src/routes/_nav/compliance/route.tsx +25 -0
  136. package/src/routes/_nav/components/route.lazy.tsx +184 -0
  137. package/src/routes/_nav/components/route.tsx +40 -0
  138. package/src/routes/_nav/dashboard/route.lazy.tsx +33 -0
  139. package/src/routes/_nav/dashboard/route.tsx +20 -0
  140. package/src/routes/_nav/dependencies/route.lazy.tsx +66 -0
  141. package/src/routes/_nav/dependencies/route.tsx +32 -0
  142. package/src/routes/_nav/developer-logs/route.lazy.tsx +6 -0
  143. package/src/routes/_nav/developer-logs/route.tsx +14 -0
  144. package/src/routes/_nav/docs/route.lazy.tsx +30 -0
  145. package/src/routes/_nav/docs/route.tsx +20 -0
  146. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +355 -0
  147. package/src/routes/_nav/knowledge-public/bookmarks/route.tsx +40 -0
  148. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +245 -0
  149. package/src/routes/_nav/knowledge-public/compliance/route.tsx +40 -0
  150. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +242 -0
  151. package/src/routes/_nav/knowledge-public/legal/route.tsx +40 -0
  152. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +235 -0
  153. package/src/routes/_nav/licenses/$licenseId/route.tsx +25 -0
  154. package/src/routes/_nav/licenses/new/route.lazy.tsx +237 -0
  155. package/src/routes/_nav/licenses/route.lazy.tsx +143 -0
  156. package/src/routes/_nav/licenses/route.tsx +25 -0
  157. package/src/routes/_nav/market/route.lazy.tsx +31 -0
  158. package/src/routes/_nav/market/route.tsx +20 -0
  159. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +305 -0
  160. package/src/routes/_nav/monitoring/ai-usage/route.tsx +3 -0
  161. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +111 -0
  162. package/src/routes/_nav/monitoring/health/route.lazy.tsx +260 -0
  163. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +165 -0
  164. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +240 -0
  165. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +138 -0
  166. package/src/routes/_nav/monitoring/route.lazy.tsx +68 -0
  167. package/src/routes/_nav/monitoring/route.tsx +20 -0
  168. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +399 -0
  169. package/src/routes/_nav/monitoring/sentry/route.tsx +20 -0
  170. package/src/routes/_nav/permissions/route.lazy.tsx +504 -0
  171. package/src/routes/_nav/permissions/route.tsx +26 -0
  172. package/src/routes/_nav/route.lazy.tsx +31 -0
  173. package/src/routes/_nav/route.tsx +34 -0
  174. package/src/routes/_nav/settings/route.lazy.tsx +30 -0
  175. package/src/routes/_nav/settings/route.tsx +20 -0
  176. package/src/routes/_nav/sso/route.lazy.tsx +145 -0
  177. package/src/routes/_nav/sso/route.tsx +25 -0
  178. package/src/routes/_nav/users/$userId/route.lazy.tsx +432 -0
  179. package/src/routes/_nav/users/$userId/route.tsx +27 -0
  180. package/src/routes/_nav/users/-components/users-table.tsx +38 -0
  181. package/src/routes/_nav/users/route.lazy.tsx +74 -0
  182. package/src/routes/_nav/users/route.tsx +43 -0
  183. package/src/routes/api/compliance-download/$jobId.ts +35 -0
  184. package/src/routes/api/observability/errors.ts +41 -0
  185. package/src/routes/api/observability/health.ts +30 -0
  186. package/src/routes/api.auth.$.ts +31 -0
  187. package/src/routes/api.sentry-test.ts +33 -0
  188. package/src/routes/index/route.lazy.tsx +22 -0
  189. package/src/routes/index/route.tsx +24 -0
  190. package/src/routes/login/route.lazy.tsx +46 -0
  191. package/src/routes/login/route.tsx +28 -0
  192. package/src/routes/sentry-tunnel.ts +61 -0
  193. package/src/routes/setup/route.lazy.tsx +94 -0
  194. package/src/routes/setup/route.tsx +28 -0
  195. package/src/server/__tests__/admin-ai-alerts.test.ts +113 -0
  196. package/src/server/__tests__/licenses-issue.test.ts +98 -0
  197. package/src/server/admin-ai-alerts.ts +177 -0
  198. package/src/server/admin-ai-usage.ts +134 -0
  199. package/src/server/auth-helpers.ts +51 -0
  200. package/src/server/entitlements.ts +24 -0
  201. package/src/server/knowledge-public/canonical-id.ts +55 -0
  202. package/src/server/knowledge-public/github-sync.ts +86 -0
  203. package/src/server/license-issue.ts +78 -0
  204. package/src/server/middlewares/request.ts +26 -0
  205. package/src/server/observability/debug-middleware.ts +39 -0
  206. package/src/server/observability/error-reporter.ts +99 -0
  207. package/src/server/observability/health-check.ts +140 -0
  208. package/src/server/observability/index.ts +33 -0
  209. package/src/server/observability/logger.ts +49 -0
  210. package/src/server/observability/metrics.ts +105 -0
  211. package/src/server/observability/request-middleware.ts +125 -0
  212. package/src/server/observability/timing.ts +42 -0
  213. package/src/server/pro-services.ts +32 -0
  214. package/src/server/serverFns/admin-billing.ts +94 -0
  215. package/src/server/serverFns/ai-provider/create.ts +92 -0
  216. package/src/server/serverFns/ai-provider/delete.ts +42 -0
  217. package/src/server/serverFns/ai-provider/get.ts +35 -0
  218. package/src/server/serverFns/ai-provider/list.ts +92 -0
  219. package/src/server/serverFns/ai-provider/update.ts +73 -0
  220. package/src/server/serverFns/anyhub/market.ts +64 -0
  221. package/src/server/serverFns/audit.ts +57 -0
  222. package/src/server/serverFns/auth/session.ts +26 -0
  223. package/src/server/serverFns/auth/setup.ts +73 -0
  224. package/src/server/serverFns/compliance.ts +85 -0
  225. package/src/server/serverFns/components/import.ts +197 -0
  226. package/src/server/serverFns/components/list.ts +51 -0
  227. package/src/server/serverFns/components/mutations.ts +84 -0
  228. package/src/server/serverFns/dependency/helpers.ts +245 -0
  229. package/src/server/serverFns/dependency/list.ts +127 -0
  230. package/src/server/serverFns/dependency/sync.ts +123 -0
  231. package/src/server/serverFns/dependency/update.ts +51 -0
  232. package/src/server/serverFns/developer-log-auto-generate.ts +176 -0
  233. package/src/server/serverFns/developer-logs.ts +132 -0
  234. package/src/server/serverFns/knowledge-public/bookmarks/create.ts +53 -0
  235. package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +31 -0
  236. package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +32 -0
  237. package/src/server/serverFns/knowledge-public/bookmarks/list.ts +66 -0
  238. package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +56 -0
  239. package/src/server/serverFns/knowledge-public/bookmarks/update.ts +50 -0
  240. package/src/server/serverFns/knowledge-public/compliance/create.ts +58 -0
  241. package/src/server/serverFns/knowledge-public/compliance/delete.ts +31 -0
  242. package/src/server/serverFns/knowledge-public/compliance/detail.ts +32 -0
  243. package/src/server/serverFns/knowledge-public/compliance/list.ts +59 -0
  244. package/src/server/serverFns/knowledge-public/compliance/update.ts +53 -0
  245. package/src/server/serverFns/knowledge-public/legal/create.ts +62 -0
  246. package/src/server/serverFns/knowledge-public/legal/delete.ts +31 -0
  247. package/src/server/serverFns/knowledge-public/legal/detail.ts +32 -0
  248. package/src/server/serverFns/knowledge-public/legal/list.ts +60 -0
  249. package/src/server/serverFns/knowledge-public/legal/update.ts +55 -0
  250. package/src/server/serverFns/licenses.ts +92 -0
  251. package/src/server/serverFns/locale.ts +10 -0
  252. package/src/server/serverFns/monitoring/check-ai-alerts.ts +13 -0
  253. package/src/server/serverFns/monitoring/get-ai-usage-overview.ts +25 -0
  254. package/src/server/serverFns/monitoring/get-errors.ts +43 -0
  255. package/src/server/serverFns/monitoring/get-health-settings.ts +35 -0
  256. package/src/server/serverFns/monitoring/get-health.ts +55 -0
  257. package/src/server/serverFns/monitoring/get-metrics.ts +46 -0
  258. package/src/server/serverFns/monitoring/get-request-logs.ts +66 -0
  259. package/src/server/serverFns/monitoring/report-error.ts +55 -0
  260. package/src/server/serverFns/monitoring/reset.ts +70 -0
  261. package/src/server/serverFns/permissions.ts +214 -0
  262. package/src/server/serverFns/sentry/config.ts +88 -0
  263. package/src/server/serverFns/sso-config.ts +83 -0
  264. package/src/server/serverFns/user/create.ts +40 -0
  265. package/src/server/serverFns/user/delete.ts +29 -0
  266. package/src/server/serverFns/user/detail.ts +45 -0
  267. package/src/server/serverFns/user/list.ts +56 -0
  268. package/src/server/serverFns/user/reset-password.ts +28 -0
  269. package/src/server/serverFns/user/update-role.ts +26 -0
  270. package/src/server/serverFns/user/update-status.ts +26 -0
  271. package/src/server.ts +27 -0
  272. package/src/start.ts +85 -0
  273. package/src/styles/_keyframe-animations.scss +91 -0
  274. package/src/styles/_variables.scss +296 -0
  275. package/src/styles/globals.css +200 -0
  276. package/src/types/start-authjs.d.ts +27 -0
@@ -0,0 +1,147 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ // apps/admin/src/components/ai-provider/model-list-editor.tsx
19
+ import { Plus, Trash2 } from "lucide-react"
20
+ import { Button } from "@/components/ui/button"
21
+ import { Input } from "@/components/ui/input"
22
+ import { cn } from "@/lib/utils"
23
+ import {
24
+ MODEL_CAPABILITY_OPTIONS,
25
+ type AiModelMeta,
26
+ type ModelCapability,
27
+ } from "@allbluecn/kernel"
28
+
29
+ export function ModelListEditor({
30
+ models,
31
+ onChange,
32
+ }: {
33
+ models: AiModelMeta[]
34
+ onChange: (next: AiModelMeta[]) => void
35
+ }) {
36
+ function update(idx: number, patch: Partial<AiModelMeta>) {
37
+ onChange(models.map((m, i) => (i === idx ? { ...m, ...patch } : m)))
38
+ }
39
+ function remove(idx: number) {
40
+ onChange(models.filter((_, i) => i !== idx))
41
+ }
42
+ function add() {
43
+ onChange([
44
+ ...models,
45
+ {
46
+ id: "",
47
+ displayName: "",
48
+ capabilities: ["text"],
49
+ defaultEnabled: false,
50
+ },
51
+ ])
52
+ }
53
+ function toggleCapability(idx: number, cap: ModelCapability) {
54
+ const m = models[idx]!
55
+ const caps = m.capabilities ?? []
56
+ update(idx, {
57
+ capabilities: caps.includes(cap)
58
+ ? caps.filter((c) => c !== cap)
59
+ : [...caps, cap],
60
+ })
61
+ }
62
+
63
+ return (
64
+ <div className="space-y-3">
65
+ {models.map((m, idx) => (
66
+ <div key={idx} className="space-y-2 rounded-lg border bg-card p-3">
67
+ <div className="flex items-center gap-2">
68
+ <Input
69
+ value={m.id}
70
+ onChange={(e) => update(idx, { id: e.target.value })}
71
+ placeholder="模型 ID(如 gpt-4o)"
72
+ className="font-mono text-xs"
73
+ />
74
+ <Input
75
+ value={m.displayName}
76
+ onChange={(e) => update(idx, { displayName: e.target.value })}
77
+ placeholder="显示名(如 GPT-4o)"
78
+ className="text-xs"
79
+ />
80
+ <Button
81
+ type="button"
82
+ variant="ghost"
83
+ size="icon"
84
+ onClick={() => remove(idx)}
85
+ className="shrink-0 text-destructive hover:bg-destructive/10"
86
+ aria-label="删除模型"
87
+ >
88
+ <Trash2 className="size-4" />
89
+ </Button>
90
+ </div>
91
+ <div className="flex flex-wrap items-center gap-2">
92
+ <Input
93
+ type="number"
94
+ value={m.contextWindow ?? ""}
95
+ onChange={(e) =>
96
+ update(idx, {
97
+ contextWindow: e.target.value
98
+ ? Number(e.target.value)
99
+ : undefined,
100
+ })
101
+ }
102
+ placeholder="上下文窗口(tokens)"
103
+ className="h-8 w-44 text-xs"
104
+ />
105
+ <div className="flex flex-wrap gap-1">
106
+ {MODEL_CAPABILITY_OPTIONS.map((c) => {
107
+ const active = m.capabilities?.includes(c.value)
108
+ return (
109
+ <button
110
+ key={c.value}
111
+ type="button"
112
+ onClick={() => toggleCapability(idx, c.value)}
113
+ className={cn(
114
+ "rounded-md border px-2 py-0.5 text-[11px] transition-colors",
115
+ active
116
+ ? "border-primary bg-primary/10 text-primary"
117
+ : "border-border hover:bg-muted",
118
+ )}
119
+ >
120
+ {c.label}
121
+ </button>
122
+ )
123
+ })}
124
+ </div>
125
+ <label className="ml-auto flex items-center gap-1 text-xs">
126
+ <input
127
+ type="checkbox"
128
+ checked={m.defaultEnabled ?? false}
129
+ onChange={(e) =>
130
+ update(idx, { defaultEnabled: e.target.checked })
131
+ }
132
+ />
133
+ 默认启用
134
+ </label>
135
+ </div>
136
+ </div>
137
+ ))}
138
+ <Button type="button" variant="outline" size="sm" onClick={add}>
139
+ <Plus className="size-4" />
140
+ 添加模型
141
+ </Button>
142
+ {models.length === 0 && (
143
+ <p className="text-xs text-muted-foreground">暂未添加模型,可留空</p>
144
+ )}
145
+ </div>
146
+ )
147
+ }
@@ -0,0 +1,434 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ // apps/admin/src/components/ai-provider/provider-form.tsx
19
+ import { useState } from "react"
20
+ import { useTranslation } from 'react-i18next'
21
+ import { useMutation } from "@tanstack/react-query"
22
+ import { Link, useNavigate, useRouter } from "@tanstack/react-router"
23
+ import { ArrowLeft, Loader2, Save } from "lucide-react"
24
+ import { toast } from '@/components/ui/sonner'
25
+ import {
26
+ ADAPTER_KINDS,
27
+ PROVIDER_CATEGORIES,
28
+ type AiModelMeta,
29
+ type AdapterKind,
30
+ type ProviderCategory,
31
+ } from "@allbluecn/kernel"
32
+ import { Button } from "@/components/ui/button"
33
+ import { Input } from "@/components/ui/input"
34
+ import { Textarea } from "@/components/ui/textarea"
35
+ import { Switch } from "@/components/ui/switch"
36
+ import {
37
+ Select,
38
+ SelectContent,
39
+ SelectItem,
40
+ SelectTrigger,
41
+ SelectValue,
42
+ } from "@/components/ui/select"
43
+ import {
44
+ Field,
45
+ FieldContent,
46
+ FieldDescription,
47
+ FieldGroup,
48
+ FieldLabel,
49
+ FieldSet,
50
+ FieldLegend,
51
+ FieldSeparator,
52
+ } from "@/components/ui/field"
53
+ import { createAiProviderAdminFn } from "@/server/serverFns/ai-provider/create"
54
+ import { updateAiProviderAdminFn } from "@/server/serverFns/ai-provider/update"
55
+ import { ModelListEditor } from "./model-list-editor"
56
+ import { ProviderIcon } from "./provider-icon"
57
+
58
+ type ProviderFormValues = {
59
+ providerId: string
60
+ displayName: string
61
+ brandName: string
62
+ description: string
63
+ category: ProviderCategory
64
+ adapterKind: AdapterKind
65
+ badge: string
66
+ defaultBaseUrl: string
67
+ keyUrl: string
68
+ docsUrl: string
69
+ keyPlaceholder: string
70
+ iconSlug: string
71
+ brandColor: string
72
+ isLocal: boolean
73
+ enabled: boolean
74
+ sortOrder: number
75
+ models: AiModelMeta[]
76
+ }
77
+
78
+ const DEFAULT_VALUES: ProviderFormValues = {
79
+ providerId: "",
80
+ displayName: "",
81
+ brandName: "",
82
+ description: "",
83
+ category: "popular",
84
+ adapterKind: "openai-compatible",
85
+ badge: "",
86
+ defaultBaseUrl: "",
87
+ keyUrl: "",
88
+ docsUrl: "",
89
+ keyPlaceholder: "",
90
+ iconSlug: "",
91
+ brandColor: "#6467F2",
92
+ isLocal: false,
93
+ enabled: true,
94
+ sortOrder: 0,
95
+ models: [],
96
+ }
97
+
98
+ export function ProviderForm({
99
+ mode,
100
+ initial,
101
+ providerDbId,
102
+ }: {
103
+ mode: "create" | "edit"
104
+ initial?: Partial<ProviderFormValues>
105
+ providerDbId?: string
106
+ }) {
107
+ const { t } = useTranslation('admin-ai')
108
+ const navigate = useNavigate()
109
+ const router = useRouter()
110
+ const [values, setValues] = useState<ProviderFormValues>({
111
+ ...DEFAULT_VALUES,
112
+ ...initial,
113
+ } as ProviderFormValues)
114
+
115
+ function patch(p: Partial<ProviderFormValues>) {
116
+ setValues((v) => ({ ...v, ...p }))
117
+ }
118
+
119
+ const saveMutation = useMutation({
120
+ mutationFn: async () => {
121
+ if (mode === "create") {
122
+ return createAiProviderAdminFn({ data: values })
123
+ }
124
+ return updateAiProviderAdminFn({
125
+ data: { id: providerDbId!, ...values },
126
+ })
127
+ },
128
+ onSuccess: async () => {
129
+ toast.success(mode === "create" ? t('form.toast.created') : t('form.toast.updated'))
130
+ await router.invalidate()
131
+ navigate({ to: "/ai-providers" })
132
+ },
133
+ onError: (err) =>
134
+ toast.error(t('form.toast.saveFailed', { error: err instanceof Error ? err.message : t('status.error') })),
135
+ })
136
+
137
+ function handleSubmit() {
138
+ if (!values.providerId.trim()) return toast.error(t('form.validation.providerIdRequired'))
139
+ if (!values.displayName.trim()) return toast.error(t('form.validation.displayNameRequired'))
140
+ saveMutation.mutate()
141
+ }
142
+
143
+ return (
144
+ <div className="mx-auto w-3/5 space-y-6">
145
+ <div className="sticky top-0 z-20 flex items-center gap-2 border-b bg-background pt-6 pb-3">
146
+ <Button asChild variant="ghost" size="icon">
147
+ <Link to="/ai-providers" aria-label="返回">
148
+ <ArrowLeft className="size-4" />
149
+ </Link>
150
+ </Button>
151
+ <h2 className="text-lg font-semibold">
152
+ {mode === "create" ? t('form.addProvider') : t('form.editProvider')}
153
+ </h2>
154
+ </div>
155
+
156
+ <FieldSet>
157
+ <FieldLegend>{t('form.basicInfo')}</FieldLegend>
158
+ <FieldGroup>
159
+ <Field orientation="horizontal">
160
+ <FieldContent>
161
+ <FieldLabel>{t('form.providerId')}</FieldLabel>
162
+ <FieldDescription>
163
+ {t('form.providerIdDesc')}
164
+ {mode === "edit" && " " + t('form.providerIdEditNote')}
165
+ </FieldDescription>
166
+ </FieldContent>
167
+ <Input
168
+ value={values.providerId}
169
+ onChange={(e) => patch({ providerId: e.target.value })}
170
+ placeholder={t('form.providerIdPlaceholder')}
171
+ disabled={mode === "edit"}
172
+ className="w-60 font-mono"
173
+ />
174
+ </Field>
175
+
176
+ <Field orientation="horizontal">
177
+ <FieldContent>
178
+ <FieldLabel>{t('form.preview')}</FieldLabel>
179
+ <FieldDescription>{t('form.previewDesc')}</FieldDescription>
180
+ </FieldContent>
181
+ <ProviderIcon
182
+ provider={{
183
+ providerId: values.providerId || "preview",
184
+ brandName: values.brandName || "P",
185
+ }}
186
+ size={40}
187
+ />
188
+ </Field>
189
+
190
+ <Field orientation="horizontal">
191
+ <FieldContent>
192
+ <FieldLabel>{t('form.displayName')}</FieldLabel>
193
+ <FieldDescription>{t('form.displayNameDesc')}</FieldDescription>
194
+ </FieldContent>
195
+ <Input
196
+ value={values.displayName}
197
+ onChange={(e) => patch({ displayName: e.target.value })}
198
+ className="w-60"
199
+ />
200
+ </Field>
201
+
202
+ <Field orientation="horizontal">
203
+ <FieldContent>
204
+ <FieldLabel>{t('form.brandName')}</FieldLabel>
205
+ <FieldDescription>{t('form.brandNameDesc')}</FieldDescription>
206
+ </FieldContent>
207
+ <Input
208
+ value={values.brandName}
209
+ onChange={(e) => patch({ brandName: e.target.value })}
210
+ className="w-60"
211
+ />
212
+ </Field>
213
+
214
+ <Field>
215
+ <FieldContent>
216
+ <FieldLabel>{t('form.description')}</FieldLabel>
217
+ <FieldDescription>{t('form.descriptionDesc')}</FieldDescription>
218
+ </FieldContent>
219
+ <Textarea
220
+ value={values.description}
221
+ onChange={(e) => patch({ description: e.target.value })}
222
+ rows={2}
223
+ />
224
+ </Field>
225
+
226
+ <FieldSeparator />
227
+
228
+ <Field orientation="horizontal">
229
+ <FieldContent>
230
+ <FieldLabel>{t('form.category')}</FieldLabel>
231
+ </FieldContent>
232
+ <Select
233
+ value={values.category}
234
+ onValueChange={(v) => patch({ category: v as ProviderCategory })}
235
+ >
236
+ <SelectTrigger className="w-40">
237
+ <SelectValue />
238
+ </SelectTrigger>
239
+ <SelectContent>
240
+ {PROVIDER_CATEGORIES.map((c) => (
241
+ <SelectItem key={c.value} value={c.value}>
242
+ {c.label}
243
+ </SelectItem>
244
+ ))}
245
+ </SelectContent>
246
+ </Select>
247
+ </Field>
248
+
249
+ <Field orientation="horizontal">
250
+ <FieldContent>
251
+ <FieldLabel>{t('form.adapterKind')}</FieldLabel>
252
+ <FieldDescription>{t('form.adapterKindDesc')}</FieldDescription>
253
+ </FieldContent>
254
+ <Select
255
+ value={values.adapterKind}
256
+ onValueChange={(v) => patch({ adapterKind: v as AdapterKind })}
257
+ >
258
+ <SelectTrigger className="w-60">
259
+ <SelectValue />
260
+ </SelectTrigger>
261
+ <SelectContent>
262
+ {ADAPTER_KINDS.map((k) => (
263
+ <SelectItem key={k.value} value={k.value}>
264
+ {k.label}
265
+ </SelectItem>
266
+ ))}
267
+ </SelectContent>
268
+ </Select>
269
+ </Field>
270
+
271
+ <Field orientation="horizontal">
272
+ <FieldContent>
273
+ <FieldLabel>{t('form.badge')}</FieldLabel>
274
+ <FieldDescription>{t('form.badgeDesc')}</FieldDescription>
275
+ </FieldContent>
276
+ <Input
277
+ value={values.badge}
278
+ onChange={(e) => patch({ badge: e.target.value })}
279
+ className="w-40"
280
+ placeholder={t('form.badgePlaceholder')}
281
+ />
282
+ </Field>
283
+
284
+ <Field orientation="horizontal">
285
+ <FieldContent>
286
+ <FieldLabel>{t('form.brandColor')}</FieldLabel>
287
+ <FieldDescription>{t('form.brandColorDesc')}</FieldDescription>
288
+ </FieldContent>
289
+ <div className="flex items-center gap-2">
290
+ <input
291
+ type="color"
292
+ value={values.brandColor}
293
+ onChange={(e) => patch({ brandColor: e.target.value })}
294
+ className="size-9 rounded border"
295
+ />
296
+ <Input
297
+ value={values.brandColor}
298
+ onChange={(e) => patch({ brandColor: e.target.value })}
299
+ className="w-28 font-mono"
300
+ />
301
+ </div>
302
+ </Field>
303
+
304
+ <Field orientation="horizontal">
305
+ <FieldContent>
306
+ <FieldLabel>{t('form.iconSlug')}</FieldLabel>
307
+ <FieldDescription>{t('form.iconSlugDesc')}</FieldDescription>
308
+ </FieldContent>
309
+ <Input
310
+ value={values.iconSlug}
311
+ onChange={(e) => patch({ iconSlug: e.target.value })}
312
+ className="w-60"
313
+ placeholder={t('form.iconSlugPlaceholder')}
314
+ />
315
+ </Field>
316
+ </FieldGroup>
317
+ </FieldSet>
318
+
319
+ <FieldSet>
320
+ <FieldLegend>{t('form.integrationConfig')}</FieldLegend>
321
+ <FieldGroup>
322
+ <Field orientation="horizontal">
323
+ <FieldContent>
324
+ <FieldLabel>{t('form.defaultBaseUrl')}</FieldLabel>
325
+ <FieldDescription>{t('form.defaultBaseUrlDesc')}</FieldDescription>
326
+ </FieldContent>
327
+ <Input
328
+ value={values.defaultBaseUrl}
329
+ onChange={(e) => patch({ defaultBaseUrl: e.target.value })}
330
+ className="w-72"
331
+ placeholder="https://api.example.com/v1"
332
+ />
333
+ </Field>
334
+
335
+ <Field orientation="horizontal">
336
+ <FieldContent>
337
+ <FieldLabel>{t('form.keyUrl')}</FieldLabel>
338
+ </FieldContent>
339
+ <Input
340
+ value={values.keyUrl}
341
+ onChange={(e) => patch({ keyUrl: e.target.value })}
342
+ className="w-72"
343
+ placeholder="https://..."
344
+ />
345
+ </Field>
346
+
347
+ <Field orientation="horizontal">
348
+ <FieldContent>
349
+ <FieldLabel>{t('form.docsUrl')}</FieldLabel>
350
+ </FieldContent>
351
+ <Input
352
+ value={values.docsUrl}
353
+ onChange={(e) => patch({ docsUrl: e.target.value })}
354
+ className="w-72"
355
+ placeholder="https://..."
356
+ />
357
+ </Field>
358
+
359
+ <Field orientation="horizontal">
360
+ <FieldContent>
361
+ <FieldLabel>{t('form.apikeyPlaceholder')}</FieldLabel>
362
+ </FieldContent>
363
+ <Input
364
+ value={values.keyPlaceholder}
365
+ onChange={(e) => patch({ keyPlaceholder: e.target.value })}
366
+ className="w-60"
367
+ placeholder="sk-..."
368
+ />
369
+ </Field>
370
+
371
+ <FieldSeparator />
372
+
373
+ <Field orientation="horizontal">
374
+ <FieldContent>
375
+ <FieldLabel>{t('form.localDeploy')}</FieldLabel>
376
+ <FieldDescription>{t('form.localDeployDesc')}</FieldDescription>
377
+ </FieldContent>
378
+ <Switch
379
+ checked={values.isLocal}
380
+ onCheckedChange={(v) => patch({ isLocal: v })}
381
+ />
382
+ </Field>
383
+
384
+ <Field orientation="horizontal">
385
+ <FieldContent>
386
+ <FieldLabel>{t('form.platformEnabled')}</FieldLabel>
387
+ <FieldDescription>{t('form.platformEnabledDesc')}</FieldDescription>
388
+ </FieldContent>
389
+ <Switch
390
+ checked={values.enabled}
391
+ onCheckedChange={(v) => patch({ enabled: v })}
392
+ />
393
+ </Field>
394
+
395
+ <Field orientation="horizontal">
396
+ <FieldContent>
397
+ <FieldLabel>{t('form.sortOrder')}</FieldLabel>
398
+ <FieldDescription>{t('form.sortOrderDesc')}</FieldDescription>
399
+ </FieldContent>
400
+ <Input
401
+ type="number"
402
+ value={values.sortOrder}
403
+ onChange={(e) => patch({ sortOrder: Number(e.target.value) })}
404
+ className="w-24"
405
+ />
406
+ </Field>
407
+ </FieldGroup>
408
+ </FieldSet>
409
+
410
+ <FieldSet>
411
+ <FieldLegend>{t('form.modelList')}</FieldLegend>
412
+ <FieldDescription>{t('form.modelListDesc')}</FieldDescription>
413
+ <ModelListEditor
414
+ models={values.models}
415
+ onChange={(m) => patch({ models: m })}
416
+ />
417
+ </FieldSet>
418
+
419
+ <div className="sticky bottom-0 z-20 flex items-center justify-end gap-3 border-t bg-background pt-6 pb-6">
420
+ <Button asChild variant="outline">
421
+ <Link to="/ai-providers">{t('common.cancel')}</Link>
422
+ </Button>
423
+ <Button onClick={handleSubmit} disabled={saveMutation.isPending}>
424
+ {saveMutation.isPending ? (
425
+ <Loader2 className="size-4 animate-spin" data-icon="inline-start" />
426
+ ) : (
427
+ <Save className="size-4" data-icon="inline-start" />
428
+ )}
429
+ {mode === "create" ? t('common.create') : t('common.save')}
430
+ </Button>
431
+ </div>
432
+ </div>
433
+ )
434
+ }
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ // apps/admin/src/components/ai-provider/provider-icon.tsx
19
+ import type { AiProviderMeta } from "@allbluecn/kernel"
20
+ import { cn } from "@/lib/utils"
21
+ import { ProviderLogo } from "./provider-logo"
22
+
23
+ export function ProviderIcon({
24
+ provider,
25
+ size = 40,
26
+ className,
27
+ }: {
28
+ provider: Pick<AiProviderMeta, "providerId" | "brandName">
29
+ size?: number
30
+ className?: string
31
+ }) {
32
+ return (
33
+ <div
34
+ className={cn(
35
+ "flex items-center justify-center rounded-lg bg-muted text-foreground",
36
+ className,
37
+ )}
38
+ style={{ width: size, height: size }}
39
+ >
40
+ <ProviderLogo provider={provider} size={Math.round(size * 0.6)} />
41
+ </div>
42
+ )
43
+ }