dsh-plugin-image-workbench 1.1.5 → 1.1.7

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 (173) 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 +1371 -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/server/static/index.html +15 -0
  25. package/vendor/chatgpt2api/source/.env.example +82 -82
  26. package/vendor/chatgpt2api/source/.python-version +1 -1
  27. package/vendor/chatgpt2api/source/LICENSE +21 -21
  28. package/vendor/chatgpt2api/source/README.md +391 -391
  29. package/vendor/chatgpt2api/source/VERSION +1 -1
  30. package/vendor/chatgpt2api/source/api/__init__.py +2 -2
  31. package/vendor/chatgpt2api/source/api/accounts.py +537 -537
  32. package/vendor/chatgpt2api/source/api/ai.py +217 -217
  33. package/vendor/chatgpt2api/source/api/app.py +62 -62
  34. package/vendor/chatgpt2api/source/api/errors.py +46 -46
  35. package/vendor/chatgpt2api/source/api/image_inputs.py +309 -309
  36. package/vendor/chatgpt2api/source/api/image_tasks.py +120 -120
  37. package/vendor/chatgpt2api/source/api/support.py +134 -134
  38. package/vendor/chatgpt2api/source/api/system.py +365 -365
  39. package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -50
  40. package/vendor/chatgpt2api/source/config.default.json +108 -108
  41. package/vendor/chatgpt2api/source/main.py +9 -9
  42. package/vendor/chatgpt2api/source/pyproject.toml +27 -27
  43. package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -186
  44. package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -163
  45. package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -13
  46. package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -385
  47. package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -129
  48. package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -92
  49. package/vendor/chatgpt2api/source/services/account_service.py +1757 -1757
  50. package/vendor/chatgpt2api/source/services/auth_service.py +253 -253
  51. package/vendor/chatgpt2api/source/services/backup_service.py +671 -671
  52. package/vendor/chatgpt2api/source/services/config.py +667 -667
  53. package/vendor/chatgpt2api/source/services/content_filter.py +242 -242
  54. package/vendor/chatgpt2api/source/services/cpa_service.py +315 -315
  55. package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -260
  56. package/vendor/chatgpt2api/source/services/image_service.py +376 -376
  57. package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -405
  58. package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -76
  59. package/vendor/chatgpt2api/source/services/image_task_service.py +550 -550
  60. package/vendor/chatgpt2api/source/services/log_service.py +337 -337
  61. package/vendor/chatgpt2api/source/services/model_service.py +182 -182
  62. package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -267
  63. package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -2763
  64. package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -32
  65. package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -2
  66. package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -307
  67. package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -238
  68. package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -1644
  69. package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -124
  70. package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -19
  71. package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -316
  72. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -86
  73. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -42
  74. package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -53
  75. package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -453
  76. package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -160
  77. package/vendor/chatgpt2api/source/services/proxy_service.py +648 -648
  78. package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -5
  79. package/vendor/chatgpt2api/source/services/storage/base.py +38 -38
  80. package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -194
  81. package/vendor/chatgpt2api/source/services/storage/factory.py +104 -104
  82. package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -183
  83. package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -95
  84. package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -531
  85. package/vendor/chatgpt2api/source/utils/helper.py +469 -469
  86. package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -346
  87. package/vendor/chatgpt2api/source/utils/log.py +110 -110
  88. package/vendor/chatgpt2api/source/utils/pkce.py +19 -19
  89. package/vendor/chatgpt2api/source/utils/pow.py +207 -207
  90. package/vendor/chatgpt2api/source/utils/sentinel.py +159 -159
  91. package/vendor/chatgpt2api/source/utils/turnstile.py +184 -184
  92. package/vendor/chatgpt2api/source/uv.lock +904 -904
  93. package/vendor/chatgpt2api/source/web/bun.lock +1501 -1501
  94. package/vendor/chatgpt2api/source/web/components.json +24 -24
  95. package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -17
  96. package/vendor/chatgpt2api/source/web/next.config.ts +40 -40
  97. package/vendor/chatgpt2api/source/web/package.json +58 -58
  98. package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -5
  99. package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -925
  100. package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -1307
  101. package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -224
  102. package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -444
  103. package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -9
  104. package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -9
  105. package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -172
  106. package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -169
  107. package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -39
  108. package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -59
  109. package/vendor/chatgpt2api/source/web/src/app/globals.css +230 -230
  110. package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -542
  111. package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -554
  112. package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -176
  113. package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -1799
  114. package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -732
  115. package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -50
  116. package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -101
  117. package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -356
  118. package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -30
  119. package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -341
  120. package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -447
  121. package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -71
  122. package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -491
  123. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -108
  124. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -169
  125. package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -185
  126. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -379
  127. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -127
  128. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -200
  129. package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -12
  130. package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -886
  131. package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -81
  132. package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -422
  133. package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -135
  134. package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -1069
  135. package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -49
  136. package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -24
  137. package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -434
  138. package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -46
  139. package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -18
  140. package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -14
  141. package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -263
  142. package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -256
  143. package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -41
  144. package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -59
  145. package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -46
  146. package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -61
  147. package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -27
  148. package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -267
  149. package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -132
  150. package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -13
  151. package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -19
  152. package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -24
  153. package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -149
  154. package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -38
  155. package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -175
  156. package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -143
  157. package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -29
  158. package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -91
  159. package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -269
  160. package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -18
  161. package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -127
  162. package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -6
  163. package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -85
  164. package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -926
  165. package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -26
  166. package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -25
  167. package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -93
  168. package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -89
  169. package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -6
  170. package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -112
  171. package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -33
  172. package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -318
  173. 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
+ }