dsh-plugin-image-workbench 1.1.5 → 1.1.6

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 (172) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +5 -3
  3. package/README.md +7 -3
  4. package/client.js +241 -241
  5. package/cordis.patch.yml +14 -14
  6. package/host.js +51 -0
  7. package/install.mjs +333 -387
  8. package/package.json +3 -2
  9. package/scripts/fetch-style-images.mjs +104 -0
  10. package/scripts/vendor-snapshot.py +154 -154
  11. package/server/access.py +26 -26
  12. package/server/app.py +1329 -1329
  13. package/server/catalog.py +147 -147
  14. package/server/composer.py +391 -391
  15. package/server/drafts.py +158 -158
  16. package/server/history.py +575 -575
  17. package/server/imaging.py +172 -172
  18. package/server/pool.py +1335 -1335
  19. package/server/pool_admin.py +921 -921
  20. package/server/pool_service.py +1073 -1073
  21. package/server/requirements.txt +6 -6
  22. package/server/smoke_check.py +402 -402
  23. package/server/static/access.html +202 -202
  24. package/vendor/chatgpt2api/source/.env.example +82 -82
  25. package/vendor/chatgpt2api/source/.python-version +1 -1
  26. package/vendor/chatgpt2api/source/LICENSE +21 -21
  27. package/vendor/chatgpt2api/source/README.md +391 -391
  28. package/vendor/chatgpt2api/source/VERSION +1 -1
  29. package/vendor/chatgpt2api/source/api/__init__.py +2 -2
  30. package/vendor/chatgpt2api/source/api/accounts.py +537 -537
  31. package/vendor/chatgpt2api/source/api/ai.py +217 -217
  32. package/vendor/chatgpt2api/source/api/app.py +62 -62
  33. package/vendor/chatgpt2api/source/api/errors.py +46 -46
  34. package/vendor/chatgpt2api/source/api/image_inputs.py +309 -309
  35. package/vendor/chatgpt2api/source/api/image_tasks.py +120 -120
  36. package/vendor/chatgpt2api/source/api/support.py +134 -134
  37. package/vendor/chatgpt2api/source/api/system.py +365 -365
  38. package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -50
  39. package/vendor/chatgpt2api/source/config.default.json +108 -108
  40. package/vendor/chatgpt2api/source/main.py +9 -9
  41. package/vendor/chatgpt2api/source/pyproject.toml +27 -27
  42. package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -186
  43. package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -163
  44. package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -13
  45. package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -385
  46. package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -129
  47. package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -92
  48. package/vendor/chatgpt2api/source/services/account_service.py +1757 -1757
  49. package/vendor/chatgpt2api/source/services/auth_service.py +253 -253
  50. package/vendor/chatgpt2api/source/services/backup_service.py +671 -671
  51. package/vendor/chatgpt2api/source/services/config.py +667 -667
  52. package/vendor/chatgpt2api/source/services/content_filter.py +242 -242
  53. package/vendor/chatgpt2api/source/services/cpa_service.py +315 -315
  54. package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -260
  55. package/vendor/chatgpt2api/source/services/image_service.py +376 -376
  56. package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -405
  57. package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -76
  58. package/vendor/chatgpt2api/source/services/image_task_service.py +550 -550
  59. package/vendor/chatgpt2api/source/services/log_service.py +337 -337
  60. package/vendor/chatgpt2api/source/services/model_service.py +182 -182
  61. package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -267
  62. package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -2763
  63. package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -32
  64. package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -2
  65. package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -307
  66. package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -238
  67. package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -1644
  68. package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -124
  69. package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -19
  70. package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -316
  71. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -86
  72. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -42
  73. package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -53
  74. package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -453
  75. package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -160
  76. package/vendor/chatgpt2api/source/services/proxy_service.py +648 -648
  77. package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -5
  78. package/vendor/chatgpt2api/source/services/storage/base.py +38 -38
  79. package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -194
  80. package/vendor/chatgpt2api/source/services/storage/factory.py +104 -104
  81. package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -183
  82. package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -95
  83. package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -531
  84. package/vendor/chatgpt2api/source/utils/helper.py +469 -469
  85. package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -346
  86. package/vendor/chatgpt2api/source/utils/log.py +110 -110
  87. package/vendor/chatgpt2api/source/utils/pkce.py +19 -19
  88. package/vendor/chatgpt2api/source/utils/pow.py +207 -207
  89. package/vendor/chatgpt2api/source/utils/sentinel.py +159 -159
  90. package/vendor/chatgpt2api/source/utils/turnstile.py +184 -184
  91. package/vendor/chatgpt2api/source/uv.lock +904 -904
  92. package/vendor/chatgpt2api/source/web/bun.lock +1501 -1501
  93. package/vendor/chatgpt2api/source/web/components.json +24 -24
  94. package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -17
  95. package/vendor/chatgpt2api/source/web/next.config.ts +40 -40
  96. package/vendor/chatgpt2api/source/web/package.json +58 -58
  97. package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -5
  98. package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -925
  99. package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -1307
  100. package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -224
  101. package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -444
  102. package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -9
  103. package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -9
  104. package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -172
  105. package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -169
  106. package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -39
  107. package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -59
  108. package/vendor/chatgpt2api/source/web/src/app/globals.css +230 -230
  109. package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -542
  110. package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -554
  111. package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -176
  112. package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -1799
  113. package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -732
  114. package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -50
  115. package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -101
  116. package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -356
  117. package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -30
  118. package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -341
  119. package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -447
  120. package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -71
  121. package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -491
  122. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -108
  123. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -169
  124. package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -185
  125. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -379
  126. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -127
  127. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -200
  128. package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -12
  129. package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -886
  130. package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -81
  131. package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -422
  132. package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -135
  133. package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -1069
  134. package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -49
  135. package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -24
  136. package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -434
  137. package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -46
  138. package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -18
  139. package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -14
  140. package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -263
  141. package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -256
  142. package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -41
  143. package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -59
  144. package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -46
  145. package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -61
  146. package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -27
  147. package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -267
  148. package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -132
  149. package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -13
  150. package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -19
  151. package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -24
  152. package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -149
  153. package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -38
  154. package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -175
  155. package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -143
  156. package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -29
  157. package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -91
  158. package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -269
  159. package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -18
  160. package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -127
  161. package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -6
  162. package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -85
  163. package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -926
  164. package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -26
  165. package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -25
  166. package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -93
  167. package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -89
  168. package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -6
  169. package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -112
  170. package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -33
  171. package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -318
  172. package/vendor/chatgpt2api/source/web/tsconfig.json +42 -42
@@ -1,422 +1,422 @@
1
- "use client";
2
-
3
- import { useEffect, useRef, useState } from "react";
4
- import { Ban, CheckCircle2, Copy, KeyRound, LoaderCircle, Pencil, Plus, Trash2 } from "lucide-react";
5
- import { toast } from "sonner";
6
-
7
- import { Badge } from "@/components/ui/badge";
8
- import { Button } from "@/components/ui/button";
9
- import { Card, CardContent } from "@/components/ui/card";
10
- import {
11
- Dialog,
12
- DialogContent,
13
- DialogDescription,
14
- DialogFooter,
15
- DialogHeader,
16
- DialogTitle,
17
- } from "@/components/ui/dialog";
18
- import { Input } from "@/components/ui/input";
19
- import { createUserKey, deleteUserKey, fetchUserKeys, updateUserKey, type UserKey } from "@/lib/api";
20
-
21
- function formatDateTime(value?: string | null) {
22
- if (!value) {
23
- return "—";
24
- }
25
- const date = new Date(value);
26
- if (Number.isNaN(date.getTime())) {
27
- return value;
28
- }
29
- return new Intl.DateTimeFormat("zh-CN", {
30
- year: "numeric",
31
- month: "2-digit",
32
- day: "2-digit",
33
- hour: "2-digit",
34
- minute: "2-digit",
35
- }).format(date);
36
- }
37
-
38
- export function UserKeysCard() {
39
- const didLoadRef = useRef(false);
40
- const [items, setItems] = useState<UserKey[]>([]);
41
- const [isLoading, setIsLoading] = useState(true);
42
- const [isDialogOpen, setIsDialogOpen] = useState(false);
43
- const [name, setName] = useState("");
44
- const [isCreating, setIsCreating] = useState(false);
45
- const [pendingIds, setPendingIds] = useState<Set<string>>(() => new Set());
46
- const [revealedKey, setRevealedKey] = useState("");
47
- const [deletingItem, setDeletingItem] = useState<UserKey | null>(null);
48
- const [editingItem, setEditingItem] = useState<UserKey | null>(null);
49
- const [editName, setEditName] = useState("");
50
- const [editKey, setEditKey] = useState("");
51
-
52
- const load = async () => {
53
- setIsLoading(true);
54
- try {
55
- const data = await fetchUserKeys();
56
- setItems(data.items);
57
- } catch (error) {
58
- toast.error(error instanceof Error ? error.message : "加载用户密钥失败");
59
- } finally {
60
- setIsLoading(false);
61
- }
62
- };
63
-
64
- useEffect(() => {
65
- if (didLoadRef.current) {
66
- return;
67
- }
68
- didLoadRef.current = true;
69
- void load();
70
- }, []);
71
-
72
- const handleCreate = async () => {
73
- setIsCreating(true);
74
- try {
75
- const data = await createUserKey(name.trim());
76
- setItems(data.items);
77
- setRevealedKey(data.key);
78
- setName("");
79
- setIsDialogOpen(false);
80
- toast.success("用户密钥已创建");
81
- } catch (error) {
82
- toast.error(error instanceof Error ? error.message : "创建用户密钥失败");
83
- } finally {
84
- setIsCreating(false);
85
- }
86
- };
87
-
88
- const setItemPending = (id: string, isPending: boolean) => {
89
- setPendingIds((current) => {
90
- const next = new Set(current);
91
- if (isPending) {
92
- next.add(id);
93
- } else {
94
- next.delete(id);
95
- }
96
- return next;
97
- });
98
- };
99
-
100
- const handleToggle = async (item: UserKey) => {
101
- setItemPending(item.id, true);
102
- try {
103
- const data = await updateUserKey(item.id, { enabled: !item.enabled });
104
- setItems(data.items);
105
- toast.success(item.enabled ? "用户密钥已禁用" : "用户密钥已启用");
106
- } catch (error) {
107
- toast.error(error instanceof Error ? error.message : "更新用户密钥失败");
108
- } finally {
109
- setItemPending(item.id, false);
110
- }
111
- };
112
-
113
- const handleDelete = async () => {
114
- if (!deletingItem) {
115
- return;
116
- }
117
- const item = deletingItem;
118
- setItemPending(item.id, true);
119
- try {
120
- const data = await deleteUserKey(item.id);
121
- setItems(data.items);
122
- setDeletingItem(null);
123
- toast.success("用户密钥已删除");
124
- } catch (error) {
125
- toast.error(error instanceof Error ? error.message : "删除用户密钥失败");
126
- } finally {
127
- setItemPending(item.id, false);
128
- }
129
- };
130
-
131
- const openEditDialog = (item: UserKey) => {
132
- setEditingItem(item);
133
- setEditName(item.name);
134
- setEditKey("");
135
- };
136
-
137
- const handleEdit = async () => {
138
- if (!editingItem) {
139
- return;
140
- }
141
- const item = editingItem;
142
- const trimmedName = editName.trim();
143
- const trimmedKey = editKey.trim();
144
- if (trimmedName === item.name && !trimmedKey) {
145
- setEditingItem(null);
146
- return;
147
- }
148
- setItemPending(item.id, true);
149
- try {
150
- const data = await updateUserKey(item.id, {
151
- ...(trimmedName !== item.name ? { name: trimmedName } : {}),
152
- ...(trimmedKey ? { key: trimmedKey } : {}),
153
- });
154
- setItems(data.items);
155
- setEditingItem(null);
156
- setEditKey("");
157
- toast.success(trimmedKey ? "用户密钥已更新" : "用户名称已更新");
158
- } catch (error) {
159
- toast.error(error instanceof Error ? error.message : "更新用户密钥失败");
160
- } finally {
161
- setItemPending(item.id, false);
162
- }
163
- };
164
-
165
- const handleCopy = async (value: string) => {
166
- try {
167
- await navigator.clipboard.writeText(value);
168
- toast.success("已复制到剪贴板");
169
- } catch {
170
- toast.error("复制失败,请手动复制");
171
- }
172
- };
173
-
174
- return (
175
- <>
176
- <Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
177
- <CardContent className="space-y-6 p-6">
178
- <div className="flex items-start justify-between gap-4">
179
- <div className="flex items-center gap-3">
180
- <div className="flex size-10 items-center justify-center rounded-xl bg-stone-100">
181
- <KeyRound className="size-5 text-stone-600" />
182
- </div>
183
- <div>
184
- <h2 className="text-lg font-semibold tracking-tight">用户密钥管理</h2>
185
- <p className="text-sm text-stone-500">为普通用户创建专用密钥;普通用户只能进入画图页,不能查看设置和号池。</p>
186
- </div>
187
- </div>
188
- <Button className="h-9 rounded-xl bg-stone-950 px-4 text-white hover:bg-stone-800" onClick={() => setIsDialogOpen(true)}>
189
- <Plus className="size-4" />
190
- 创建用户密钥
191
- </Button>
192
- </div>
193
-
194
- {revealedKey ? (
195
- <div className="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-4 text-sm text-emerald-900">
196
- <div className="font-medium">新密钥仅展示一次,请立即保存:</div>
197
- <div className="mt-3 flex flex-col gap-3 rounded-lg border border-emerald-200 bg-white/80 p-3 md:flex-row md:items-center md:justify-between">
198
- <code className="break-all font-mono text-[13px]">{revealedKey}</code>
199
- <Button
200
- type="button"
201
- variant="outline"
202
- className="h-9 rounded-xl border-emerald-200 bg-white px-4 text-emerald-700"
203
- onClick={() => void handleCopy(revealedKey)}
204
- >
205
- <Copy className="size-4" />
206
- 复制
207
- </Button>
208
- </div>
209
- </div>
210
- ) : null}
211
-
212
- {isLoading ? (
213
- <div className="flex items-center justify-center py-10">
214
- <LoaderCircle className="size-5 animate-spin text-stone-400" />
215
- </div>
216
- ) : items.length === 0 ? (
217
- <div className="rounded-xl bg-stone-50 px-6 py-10 text-center text-sm text-stone-500">
218
- 暂无普通用户密钥。点击右上角按钮后即可创建并分发给其他人。
219
- </div>
220
- ) : (
221
- <div className="space-y-3">
222
- {items.map((item) => {
223
- const isPending = pendingIds.has(item.id);
224
- return (
225
- <div key={item.id} className="flex flex-col gap-3 rounded-xl border border-stone-200 bg-white px-4 py-4 md:flex-row md:items-center md:justify-between">
226
- <div className="min-w-0 space-y-2">
227
- <div className="flex flex-wrap items-center gap-2">
228
- <div className="truncate text-sm font-medium text-stone-800">{item.name}</div>
229
- <Badge variant={item.enabled ? "success" : "secondary"} className="rounded-md">
230
- {item.enabled ? "已启用" : "已禁用"}
231
- </Badge>
232
- </div>
233
- <div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-stone-500">
234
- <span>创建时间 {formatDateTime(item.created_at)}</span>
235
- <span>最近使用 {formatDateTime(item.last_used_at)}</span>
236
- </div>
237
- </div>
238
-
239
- <div className="flex items-center gap-2">
240
- <Button
241
- type="button"
242
- variant="outline"
243
- className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
244
- onClick={() => openEditDialog(item)}
245
- disabled={isPending}
246
- >
247
- {isPending ? <LoaderCircle className="size-4 animate-spin" /> : <Pencil className="size-4" />}
248
- 编辑
249
- </Button>
250
- <Button
251
- type="button"
252
- variant="outline"
253
- className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
254
- onClick={() => void handleToggle(item)}
255
- disabled={isPending}
256
- >
257
- {isPending ? (
258
- <LoaderCircle className="size-4 animate-spin" />
259
- ) : item.enabled ? (
260
- <Ban className="size-4" />
261
- ) : (
262
- <CheckCircle2 className="size-4" />
263
- )}
264
- {item.enabled ? "禁用" : "启用"}
265
- </Button>
266
- <Button
267
- type="button"
268
- variant="outline"
269
- className="h-9 rounded-xl border-rose-200 bg-white px-4 text-rose-600 hover:bg-rose-50 hover:text-rose-700"
270
- onClick={() => setDeletingItem(item)}
271
- disabled={isPending}
272
- >
273
- {isPending ? <LoaderCircle className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
274
- 删除
275
- </Button>
276
- </div>
277
- </div>
278
- );
279
- })}
280
- </div>
281
- )}
282
- </CardContent>
283
- </Card>
284
-
285
- <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
286
- <DialogContent className="rounded-2xl p-6">
287
- <DialogHeader className="gap-2">
288
- <DialogTitle>创建用户密钥</DialogTitle>
289
- <DialogDescription className="text-sm leading-6">
290
- 可选填写一个备注名称,方便区分不同使用者;创建后会生成一条只能查看一次的原始密钥。
291
- </DialogDescription>
292
- </DialogHeader>
293
- <div className="space-y-2">
294
- <label className="text-sm font-medium text-stone-700">名称(可选)</label>
295
- <Input
296
- value={name}
297
- onChange={(event) => setName(event.target.value)}
298
- placeholder="例如:设计同学 A、运营临时账号"
299
- className="h-11 rounded-xl border-stone-200 bg-white"
300
- />
301
- </div>
302
- <DialogFooter>
303
- <Button
304
- type="button"
305
- variant="secondary"
306
- className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
307
- onClick={() => setIsDialogOpen(false)}
308
- disabled={isCreating}
309
- >
310
- 取消
311
- </Button>
312
- <Button
313
- type="button"
314
- className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
315
- onClick={() => void handleCreate()}
316
- disabled={isCreating}
317
- >
318
- {isCreating ? <LoaderCircle className="size-4 animate-spin" /> : <Plus className="size-4" />}
319
- 创建
320
- </Button>
321
- </DialogFooter>
322
- </DialogContent>
323
- </Dialog>
324
-
325
- <Dialog open={Boolean(deletingItem)} onOpenChange={(open) => (!open ? setDeletingItem(null) : null)}>
326
- <DialogContent className="rounded-2xl p-6">
327
- <DialogHeader className="gap-2">
328
- <DialogTitle>删除用户密钥</DialogTitle>
329
- <DialogDescription className="text-sm leading-6">
330
- 确认删除用户密钥「{deletingItem?.name}」吗?删除后该密钥将无法继续调用接口。
331
- </DialogDescription>
332
- </DialogHeader>
333
- <DialogFooter>
334
- <Button
335
- type="button"
336
- variant="secondary"
337
- className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
338
- onClick={() => setDeletingItem(null)}
339
- disabled={deletingItem ? pendingIds.has(deletingItem.id) : false}
340
- >
341
- 取消
342
- </Button>
343
- <Button
344
- type="button"
345
- className="h-10 rounded-xl bg-rose-600 px-5 text-white hover:bg-rose-700"
346
- onClick={() => void handleDelete()}
347
- disabled={deletingItem ? pendingIds.has(deletingItem.id) : false}
348
- >
349
- {deletingItem && pendingIds.has(deletingItem.id) ? <LoaderCircle className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
350
- 删除
351
- </Button>
352
- </DialogFooter>
353
- </DialogContent>
354
- </Dialog>
355
-
356
- <Dialog
357
- open={Boolean(editingItem)}
358
- onOpenChange={(open) => {
359
- if (!open) {
360
- setEditingItem(null);
361
- setEditKey("");
362
- }
363
- }}
364
- >
365
- <DialogContent className="rounded-2xl p-6">
366
- <DialogHeader className="gap-2">
367
- <DialogTitle>编辑用户密钥</DialogTitle>
368
- <DialogDescription className="text-sm leading-6">
369
- 可以修改备注名称;如需更换专用密钥,直接填写新的原始密钥即可。留空则保持当前密钥不变。
370
- </DialogDescription>
371
- </DialogHeader>
372
- <div className="space-y-4">
373
- <div className="space-y-2">
374
- <label className="text-sm font-medium text-stone-700">名称</label>
375
- <Input
376
- value={editName}
377
- onChange={(event) => setEditName(event.target.value)}
378
- placeholder="例如:设计同学 A、运营临时账号"
379
- className="h-11 rounded-xl border-stone-200 bg-white"
380
- />
381
- </div>
382
- <div className="space-y-2">
383
- <label className="text-sm font-medium text-stone-700">新的专用密钥(可选)</label>
384
- <Input
385
- value={editKey}
386
- onChange={(event) => setEditKey(event.target.value)}
387
- placeholder="例如:sk-your-custom-user-key"
388
- className="h-11 rounded-xl border-stone-200 bg-white font-mono"
389
- />
390
- <p className="text-xs leading-5 text-stone-500">
391
- 保存后旧密钥会立即失效,新密钥生效。系统仍只保存哈希,不会回显当前密钥。
392
- </p>
393
- </div>
394
- </div>
395
- <DialogFooter>
396
- <Button
397
- type="button"
398
- variant="secondary"
399
- className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
400
- onClick={() => {
401
- setEditingItem(null);
402
- setEditKey("");
403
- }}
404
- disabled={editingItem ? pendingIds.has(editingItem.id) : false}
405
- >
406
- 取消
407
- </Button>
408
- <Button
409
- type="button"
410
- className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
411
- onClick={() => void handleEdit()}
412
- disabled={editingItem ? pendingIds.has(editingItem.id) : false}
413
- >
414
- {editingItem && pendingIds.has(editingItem.id) ? <LoaderCircle className="size-4 animate-spin" /> : <Pencil className="size-4" />}
415
- 保存
416
- </Button>
417
- </DialogFooter>
418
- </DialogContent>
419
- </Dialog>
420
- </>
421
- );
422
- }
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { Ban, CheckCircle2, Copy, KeyRound, LoaderCircle, Pencil, Plus, Trash2 } from "lucide-react";
5
+ import { toast } from "sonner";
6
+
7
+ import { Badge } from "@/components/ui/badge";
8
+ import { Button } from "@/components/ui/button";
9
+ import { Card, CardContent } from "@/components/ui/card";
10
+ import {
11
+ Dialog,
12
+ DialogContent,
13
+ DialogDescription,
14
+ DialogFooter,
15
+ DialogHeader,
16
+ DialogTitle,
17
+ } from "@/components/ui/dialog";
18
+ import { Input } from "@/components/ui/input";
19
+ import { createUserKey, deleteUserKey, fetchUserKeys, updateUserKey, type UserKey } from "@/lib/api";
20
+
21
+ function formatDateTime(value?: string | null) {
22
+ if (!value) {
23
+ return "—";
24
+ }
25
+ const date = new Date(value);
26
+ if (Number.isNaN(date.getTime())) {
27
+ return value;
28
+ }
29
+ return new Intl.DateTimeFormat("zh-CN", {
30
+ year: "numeric",
31
+ month: "2-digit",
32
+ day: "2-digit",
33
+ hour: "2-digit",
34
+ minute: "2-digit",
35
+ }).format(date);
36
+ }
37
+
38
+ export function UserKeysCard() {
39
+ const didLoadRef = useRef(false);
40
+ const [items, setItems] = useState<UserKey[]>([]);
41
+ const [isLoading, setIsLoading] = useState(true);
42
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
43
+ const [name, setName] = useState("");
44
+ const [isCreating, setIsCreating] = useState(false);
45
+ const [pendingIds, setPendingIds] = useState<Set<string>>(() => new Set());
46
+ const [revealedKey, setRevealedKey] = useState("");
47
+ const [deletingItem, setDeletingItem] = useState<UserKey | null>(null);
48
+ const [editingItem, setEditingItem] = useState<UserKey | null>(null);
49
+ const [editName, setEditName] = useState("");
50
+ const [editKey, setEditKey] = useState("");
51
+
52
+ const load = async () => {
53
+ setIsLoading(true);
54
+ try {
55
+ const data = await fetchUserKeys();
56
+ setItems(data.items);
57
+ } catch (error) {
58
+ toast.error(error instanceof Error ? error.message : "加载用户密钥失败");
59
+ } finally {
60
+ setIsLoading(false);
61
+ }
62
+ };
63
+
64
+ useEffect(() => {
65
+ if (didLoadRef.current) {
66
+ return;
67
+ }
68
+ didLoadRef.current = true;
69
+ void load();
70
+ }, []);
71
+
72
+ const handleCreate = async () => {
73
+ setIsCreating(true);
74
+ try {
75
+ const data = await createUserKey(name.trim());
76
+ setItems(data.items);
77
+ setRevealedKey(data.key);
78
+ setName("");
79
+ setIsDialogOpen(false);
80
+ toast.success("用户密钥已创建");
81
+ } catch (error) {
82
+ toast.error(error instanceof Error ? error.message : "创建用户密钥失败");
83
+ } finally {
84
+ setIsCreating(false);
85
+ }
86
+ };
87
+
88
+ const setItemPending = (id: string, isPending: boolean) => {
89
+ setPendingIds((current) => {
90
+ const next = new Set(current);
91
+ if (isPending) {
92
+ next.add(id);
93
+ } else {
94
+ next.delete(id);
95
+ }
96
+ return next;
97
+ });
98
+ };
99
+
100
+ const handleToggle = async (item: UserKey) => {
101
+ setItemPending(item.id, true);
102
+ try {
103
+ const data = await updateUserKey(item.id, { enabled: !item.enabled });
104
+ setItems(data.items);
105
+ toast.success(item.enabled ? "用户密钥已禁用" : "用户密钥已启用");
106
+ } catch (error) {
107
+ toast.error(error instanceof Error ? error.message : "更新用户密钥失败");
108
+ } finally {
109
+ setItemPending(item.id, false);
110
+ }
111
+ };
112
+
113
+ const handleDelete = async () => {
114
+ if (!deletingItem) {
115
+ return;
116
+ }
117
+ const item = deletingItem;
118
+ setItemPending(item.id, true);
119
+ try {
120
+ const data = await deleteUserKey(item.id);
121
+ setItems(data.items);
122
+ setDeletingItem(null);
123
+ toast.success("用户密钥已删除");
124
+ } catch (error) {
125
+ toast.error(error instanceof Error ? error.message : "删除用户密钥失败");
126
+ } finally {
127
+ setItemPending(item.id, false);
128
+ }
129
+ };
130
+
131
+ const openEditDialog = (item: UserKey) => {
132
+ setEditingItem(item);
133
+ setEditName(item.name);
134
+ setEditKey("");
135
+ };
136
+
137
+ const handleEdit = async () => {
138
+ if (!editingItem) {
139
+ return;
140
+ }
141
+ const item = editingItem;
142
+ const trimmedName = editName.trim();
143
+ const trimmedKey = editKey.trim();
144
+ if (trimmedName === item.name && !trimmedKey) {
145
+ setEditingItem(null);
146
+ return;
147
+ }
148
+ setItemPending(item.id, true);
149
+ try {
150
+ const data = await updateUserKey(item.id, {
151
+ ...(trimmedName !== item.name ? { name: trimmedName } : {}),
152
+ ...(trimmedKey ? { key: trimmedKey } : {}),
153
+ });
154
+ setItems(data.items);
155
+ setEditingItem(null);
156
+ setEditKey("");
157
+ toast.success(trimmedKey ? "用户密钥已更新" : "用户名称已更新");
158
+ } catch (error) {
159
+ toast.error(error instanceof Error ? error.message : "更新用户密钥失败");
160
+ } finally {
161
+ setItemPending(item.id, false);
162
+ }
163
+ };
164
+
165
+ const handleCopy = async (value: string) => {
166
+ try {
167
+ await navigator.clipboard.writeText(value);
168
+ toast.success("已复制到剪贴板");
169
+ } catch {
170
+ toast.error("复制失败,请手动复制");
171
+ }
172
+ };
173
+
174
+ return (
175
+ <>
176
+ <Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
177
+ <CardContent className="space-y-6 p-6">
178
+ <div className="flex items-start justify-between gap-4">
179
+ <div className="flex items-center gap-3">
180
+ <div className="flex size-10 items-center justify-center rounded-xl bg-stone-100">
181
+ <KeyRound className="size-5 text-stone-600" />
182
+ </div>
183
+ <div>
184
+ <h2 className="text-lg font-semibold tracking-tight">用户密钥管理</h2>
185
+ <p className="text-sm text-stone-500">为普通用户创建专用密钥;普通用户只能进入画图页,不能查看设置和号池。</p>
186
+ </div>
187
+ </div>
188
+ <Button className="h-9 rounded-xl bg-stone-950 px-4 text-white hover:bg-stone-800" onClick={() => setIsDialogOpen(true)}>
189
+ <Plus className="size-4" />
190
+ 创建用户密钥
191
+ </Button>
192
+ </div>
193
+
194
+ {revealedKey ? (
195
+ <div className="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-4 text-sm text-emerald-900">
196
+ <div className="font-medium">新密钥仅展示一次,请立即保存:</div>
197
+ <div className="mt-3 flex flex-col gap-3 rounded-lg border border-emerald-200 bg-white/80 p-3 md:flex-row md:items-center md:justify-between">
198
+ <code className="break-all font-mono text-[13px]">{revealedKey}</code>
199
+ <Button
200
+ type="button"
201
+ variant="outline"
202
+ className="h-9 rounded-xl border-emerald-200 bg-white px-4 text-emerald-700"
203
+ onClick={() => void handleCopy(revealedKey)}
204
+ >
205
+ <Copy className="size-4" />
206
+ 复制
207
+ </Button>
208
+ </div>
209
+ </div>
210
+ ) : null}
211
+
212
+ {isLoading ? (
213
+ <div className="flex items-center justify-center py-10">
214
+ <LoaderCircle className="size-5 animate-spin text-stone-400" />
215
+ </div>
216
+ ) : items.length === 0 ? (
217
+ <div className="rounded-xl bg-stone-50 px-6 py-10 text-center text-sm text-stone-500">
218
+ 暂无普通用户密钥。点击右上角按钮后即可创建并分发给其他人。
219
+ </div>
220
+ ) : (
221
+ <div className="space-y-3">
222
+ {items.map((item) => {
223
+ const isPending = pendingIds.has(item.id);
224
+ return (
225
+ <div key={item.id} className="flex flex-col gap-3 rounded-xl border border-stone-200 bg-white px-4 py-4 md:flex-row md:items-center md:justify-between">
226
+ <div className="min-w-0 space-y-2">
227
+ <div className="flex flex-wrap items-center gap-2">
228
+ <div className="truncate text-sm font-medium text-stone-800">{item.name}</div>
229
+ <Badge variant={item.enabled ? "success" : "secondary"} className="rounded-md">
230
+ {item.enabled ? "已启用" : "已禁用"}
231
+ </Badge>
232
+ </div>
233
+ <div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-stone-500">
234
+ <span>创建时间 {formatDateTime(item.created_at)}</span>
235
+ <span>最近使用 {formatDateTime(item.last_used_at)}</span>
236
+ </div>
237
+ </div>
238
+
239
+ <div className="flex items-center gap-2">
240
+ <Button
241
+ type="button"
242
+ variant="outline"
243
+ className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
244
+ onClick={() => openEditDialog(item)}
245
+ disabled={isPending}
246
+ >
247
+ {isPending ? <LoaderCircle className="size-4 animate-spin" /> : <Pencil className="size-4" />}
248
+ 编辑
249
+ </Button>
250
+ <Button
251
+ type="button"
252
+ variant="outline"
253
+ className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
254
+ onClick={() => void handleToggle(item)}
255
+ disabled={isPending}
256
+ >
257
+ {isPending ? (
258
+ <LoaderCircle className="size-4 animate-spin" />
259
+ ) : item.enabled ? (
260
+ <Ban className="size-4" />
261
+ ) : (
262
+ <CheckCircle2 className="size-4" />
263
+ )}
264
+ {item.enabled ? "禁用" : "启用"}
265
+ </Button>
266
+ <Button
267
+ type="button"
268
+ variant="outline"
269
+ className="h-9 rounded-xl border-rose-200 bg-white px-4 text-rose-600 hover:bg-rose-50 hover:text-rose-700"
270
+ onClick={() => setDeletingItem(item)}
271
+ disabled={isPending}
272
+ >
273
+ {isPending ? <LoaderCircle className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
274
+ 删除
275
+ </Button>
276
+ </div>
277
+ </div>
278
+ );
279
+ })}
280
+ </div>
281
+ )}
282
+ </CardContent>
283
+ </Card>
284
+
285
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
286
+ <DialogContent className="rounded-2xl p-6">
287
+ <DialogHeader className="gap-2">
288
+ <DialogTitle>创建用户密钥</DialogTitle>
289
+ <DialogDescription className="text-sm leading-6">
290
+ 可选填写一个备注名称,方便区分不同使用者;创建后会生成一条只能查看一次的原始密钥。
291
+ </DialogDescription>
292
+ </DialogHeader>
293
+ <div className="space-y-2">
294
+ <label className="text-sm font-medium text-stone-700">名称(可选)</label>
295
+ <Input
296
+ value={name}
297
+ onChange={(event) => setName(event.target.value)}
298
+ placeholder="例如:设计同学 A、运营临时账号"
299
+ className="h-11 rounded-xl border-stone-200 bg-white"
300
+ />
301
+ </div>
302
+ <DialogFooter>
303
+ <Button
304
+ type="button"
305
+ variant="secondary"
306
+ className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
307
+ onClick={() => setIsDialogOpen(false)}
308
+ disabled={isCreating}
309
+ >
310
+ 取消
311
+ </Button>
312
+ <Button
313
+ type="button"
314
+ className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
315
+ onClick={() => void handleCreate()}
316
+ disabled={isCreating}
317
+ >
318
+ {isCreating ? <LoaderCircle className="size-4 animate-spin" /> : <Plus className="size-4" />}
319
+ 创建
320
+ </Button>
321
+ </DialogFooter>
322
+ </DialogContent>
323
+ </Dialog>
324
+
325
+ <Dialog open={Boolean(deletingItem)} onOpenChange={(open) => (!open ? setDeletingItem(null) : null)}>
326
+ <DialogContent className="rounded-2xl p-6">
327
+ <DialogHeader className="gap-2">
328
+ <DialogTitle>删除用户密钥</DialogTitle>
329
+ <DialogDescription className="text-sm leading-6">
330
+ 确认删除用户密钥「{deletingItem?.name}」吗?删除后该密钥将无法继续调用接口。
331
+ </DialogDescription>
332
+ </DialogHeader>
333
+ <DialogFooter>
334
+ <Button
335
+ type="button"
336
+ variant="secondary"
337
+ className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
338
+ onClick={() => setDeletingItem(null)}
339
+ disabled={deletingItem ? pendingIds.has(deletingItem.id) : false}
340
+ >
341
+ 取消
342
+ </Button>
343
+ <Button
344
+ type="button"
345
+ className="h-10 rounded-xl bg-rose-600 px-5 text-white hover:bg-rose-700"
346
+ onClick={() => void handleDelete()}
347
+ disabled={deletingItem ? pendingIds.has(deletingItem.id) : false}
348
+ >
349
+ {deletingItem && pendingIds.has(deletingItem.id) ? <LoaderCircle className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
350
+ 删除
351
+ </Button>
352
+ </DialogFooter>
353
+ </DialogContent>
354
+ </Dialog>
355
+
356
+ <Dialog
357
+ open={Boolean(editingItem)}
358
+ onOpenChange={(open) => {
359
+ if (!open) {
360
+ setEditingItem(null);
361
+ setEditKey("");
362
+ }
363
+ }}
364
+ >
365
+ <DialogContent className="rounded-2xl p-6">
366
+ <DialogHeader className="gap-2">
367
+ <DialogTitle>编辑用户密钥</DialogTitle>
368
+ <DialogDescription className="text-sm leading-6">
369
+ 可以修改备注名称;如需更换专用密钥,直接填写新的原始密钥即可。留空则保持当前密钥不变。
370
+ </DialogDescription>
371
+ </DialogHeader>
372
+ <div className="space-y-4">
373
+ <div className="space-y-2">
374
+ <label className="text-sm font-medium text-stone-700">名称</label>
375
+ <Input
376
+ value={editName}
377
+ onChange={(event) => setEditName(event.target.value)}
378
+ placeholder="例如:设计同学 A、运营临时账号"
379
+ className="h-11 rounded-xl border-stone-200 bg-white"
380
+ />
381
+ </div>
382
+ <div className="space-y-2">
383
+ <label className="text-sm font-medium text-stone-700">新的专用密钥(可选)</label>
384
+ <Input
385
+ value={editKey}
386
+ onChange={(event) => setEditKey(event.target.value)}
387
+ placeholder="例如:sk-your-custom-user-key"
388
+ className="h-11 rounded-xl border-stone-200 bg-white font-mono"
389
+ />
390
+ <p className="text-xs leading-5 text-stone-500">
391
+ 保存后旧密钥会立即失效,新密钥生效。系统仍只保存哈希,不会回显当前密钥。
392
+ </p>
393
+ </div>
394
+ </div>
395
+ <DialogFooter>
396
+ <Button
397
+ type="button"
398
+ variant="secondary"
399
+ className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
400
+ onClick={() => {
401
+ setEditingItem(null);
402
+ setEditKey("");
403
+ }}
404
+ disabled={editingItem ? pendingIds.has(editingItem.id) : false}
405
+ >
406
+ 取消
407
+ </Button>
408
+ <Button
409
+ type="button"
410
+ className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
411
+ onClick={() => void handleEdit()}
412
+ disabled={editingItem ? pendingIds.has(editingItem.id) : false}
413
+ >
414
+ {editingItem && pendingIds.has(editingItem.id) ? <LoaderCircle className="size-4 animate-spin" /> : <Pencil className="size-4" />}
415
+ 保存
416
+ </Button>
417
+ </DialogFooter>
418
+ </DialogContent>
419
+ </Dialog>
420
+ </>
421
+ );
422
+ }