sa2kit 1.6.85 → 1.6.89

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/vocaloidBooth/server/inMemoryBoothVaultStore.ts","../../../src/vocaloidBooth/server/adapters.ts","../../../src/vocaloidBooth/server/cleanup.ts","../../../src/vocaloidBooth/server/security.ts","../../../src/vocaloidBooth/server/ossIntegration.ts","../../../src/vocaloidBooth/server/audit.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { BoothUploadRecord, BoothVaultStore } from '../types';\n\nexport class InMemoryBoothVaultStore implements BoothVaultStore {\n private readonly recordsById = new Map<string, BoothUploadRecord>();\n private readonly idByCode = new Map<string, string>();\n\n async saveRecord(record: BoothUploadRecord): Promise<void> {\n this.recordsById.set(record.id, record);\n this.idByCode.set(record.matchCode, record.id);\n }\n\n async findByMatchCode(matchCode: string): Promise<BoothUploadRecord | null> {\n const id = this.idByCode.get(matchCode);\n if (!id) {\n return null;\n }\n return this.recordsById.get(id) ?? null;\n }\n\n async findByRecordId(recordId: string): Promise<BoothUploadRecord | null> {\n return this.recordsById.get(recordId) ?? null;\n }\n\n async incrementDownloadCount(recordId: string): Promise<void> {\n const record = this.recordsById.get(recordId);\n if (!record) {\n return;\n }\n\n this.recordsById.set(recordId, {\n ...record,\n downloadCount: record.downloadCount + 1,\n });\n }\n\n async existsByMatchCode(matchCode: string): Promise<boolean> {\n return this.idByCode.has(matchCode);\n }\n\n async listActiveRecords(): Promise<BoothUploadRecord[]> {\n return Array.from(this.recordsById.values()).filter((record) => record.status === 'active');\n }\n\n async updateStatus(recordId: string, status: BoothUploadRecord['status']): Promise<void> {\n const record = this.recordsById.get(recordId);\n if (!record) return;\n\n this.recordsById.set(recordId, {\n ...record,\n status,\n });\n }\n}\n","import type { BoothFileItem, BoothUploadRecord, BoothVaultStore } from '../types';\n\nexport interface BoothVaultRecordRepository {\n create(record: BoothUploadRecord): Promise<void>;\n findByMatchCode(matchCode: string): Promise<BoothUploadRecord | null>;\n findByRecordId?(recordId: string): Promise<BoothUploadRecord | null>;\n updateDownloadCount(recordId: string, downloadCount: number): Promise<void>;\n existsByMatchCode(matchCode: string): Promise<boolean>;\n}\n\nexport interface BoothObjectStorageProvider {\n save(file: File | Blob | Buffer, objectKey: string, contentType?: string): Promise<string>;\n getSignedDownloadUrl(objectKey: string, expiresInSeconds?: number): Promise<string>;\n delete(objectKey: string): Promise<void>;\n}\n\nexport class RepositoryBoothVaultStore implements BoothVaultStore {\n constructor(private readonly repository: BoothVaultRecordRepository) {}\n\n saveRecord(record: BoothUploadRecord): Promise<void> {\n return this.repository.create(record);\n }\n\n findByMatchCode(matchCode: string): Promise<BoothUploadRecord | null> {\n return this.repository.findByMatchCode(matchCode);\n }\n\n async incrementDownloadCount(recordId: string): Promise<void> {\n const record = await this.findByRecordId(recordId);\n if (!record) return;\n return this.repository.updateDownloadCount(recordId, record.downloadCount + 1);\n }\n\n existsByMatchCode(matchCode: string): Promise<boolean> {\n return this.repository.existsByMatchCode(matchCode);\n }\n\n async findByRecordId(recordId: string): Promise<BoothUploadRecord | null> {\n if (this.repository.findByRecordId) {\n return this.repository.findByRecordId(recordId);\n }\n return null;\n }\n}\n\nexport interface BoothSignedFile extends BoothFileItem {\n signedDownloadUrl: string;\n}\n\nexport const signRecordFiles = async (\n record: BoothUploadRecord,\n storage: BoothObjectStorageProvider,\n expiresInSeconds = 60 * 30\n): Promise<BoothSignedFile[]> => {\n const signed = await Promise.all(\n record.files.map(async (file) => ({\n ...file,\n signedDownloadUrl: await storage.getSignedDownloadUrl(file.objectKey, expiresInSeconds),\n }))\n );\n\n return signed;\n};\n","import type { BoothUploadRecord, BoothVaultStore } from '../types';\n\nexport interface BoothExpiryStore extends BoothVaultStore {\n listActiveRecords?(): Promise<BoothUploadRecord[]>;\n updateStatus?(recordId: string, status: BoothUploadRecord['status']): Promise<void>;\n}\n\nexport interface ExpireResult {\n scanned: number;\n expired: number;\n}\n\nexport const expireBoothRecords = async (\n store: BoothExpiryStore,\n now = Date.now(),\n onExpired?: (record: BoothUploadRecord) => void\n): Promise<ExpireResult> => {\n const records = store.listActiveRecords ? await store.listActiveRecords() : [];\n\n let expired = 0;\n for (const record of records) {\n if (record.status !== 'active') continue;\n if (new Date(record.expiresAt).getTime() <= now) {\n expired += 1;\n if (store.updateStatus) {\n // eslint-disable-next-line no-await-in-loop\n await store.updateStatus(record.id, 'expired');\n }\n onExpired?.(record);\n }\n }\n\n return {\n scanned: records.length,\n expired,\n };\n};\n","export interface BoothRedeemGuardOptions {\n maxAttempts?: number;\n windowMs?: number;\n blockMs?: number;\n}\n\ninterface AttemptState {\n attempts: number[];\n blockedUntil?: number;\n}\n\nexport class BoothRedeemGuard {\n private readonly maxAttempts: number;\n private readonly windowMs: number;\n private readonly blockMs: number;\n private readonly state = new Map<string, AttemptState>();\n\n constructor(options: BoothRedeemGuardOptions = {}) {\n this.maxAttempts = options.maxAttempts ?? 8;\n this.windowMs = options.windowMs ?? 60_000;\n this.blockMs = options.blockMs ?? 5 * 60_000;\n }\n\n assertAllowed(subjectKey: string, now = Date.now()): void {\n const state = this.getState(subjectKey, now);\n if (state.blockedUntil && state.blockedUntil > now) {\n const seconds = Math.ceil((state.blockedUntil - now) / 1000);\n throw new Error(`Too many attempts, retry in ${seconds}s`);\n }\n }\n\n registerAttempt(subjectKey: string, success: boolean, now = Date.now()): void {\n const state = this.getState(subjectKey, now);\n\n if (success) {\n this.state.delete(subjectKey);\n return;\n }\n\n state.attempts.push(now);\n state.attempts = state.attempts.filter((t) => now - t <= this.windowMs);\n\n if (state.attempts.length >= this.maxAttempts) {\n state.blockedUntil = now + this.blockMs;\n state.attempts = [];\n }\n\n this.state.set(subjectKey, state);\n }\n\n private getState(subjectKey: string, now: number): AttemptState {\n const state = this.state.get(subjectKey) ?? { attempts: [] };\n state.attempts = state.attempts.filter((t) => now - t <= this.windowMs);\n\n if (state.blockedUntil && state.blockedUntil <= now) {\n delete state.blockedUntil;\n }\n\n return state;\n }\n}\n\nexport interface ValidateUploadFilesOptions {\n maxFiles?: number;\n maxSingleFileSizeBytes?: number;\n maxTotalSizeBytes?: number;\n allowedExtensions?: string[];\n}\n\nexport interface UploadLikeFile {\n fileName: string;\n size: number;\n}\n\nconst ext = (name: string): string => {\n const i = name.lastIndexOf('.');\n return i >= 0 ? name.slice(i + 1).toLowerCase() : '';\n};\n\nexport const validateUploadFiles = (\n files: UploadLikeFile[],\n options: ValidateUploadFilesOptions = {}\n): void => {\n const maxFiles = options.maxFiles ?? 20;\n const maxSingle = options.maxSingleFileSizeBytes ?? 2 * 1024 * 1024 * 1024;\n const maxTotal = options.maxTotalSizeBytes ?? 5 * 1024 * 1024 * 1024;\n const allowed = options.allowedExtensions?.map((e) => e.toLowerCase());\n\n if (files.length === 0) {\n throw new Error('No files uploaded');\n }\n if (files.length > maxFiles) {\n throw new Error(`Too many files (max ${maxFiles})`);\n }\n\n const total = files.reduce((sum, f) => sum + f.size, 0);\n if (total > maxTotal) {\n throw new Error('Total upload size exceeded');\n }\n\n for (const file of files) {\n if (file.size > maxSingle) {\n throw new Error(`File too large: ${file.fileName}`);\n }\n if (allowed && allowed.length > 0 && !allowed.includes(ext(file.fileName))) {\n throw new Error(`File extension not allowed: ${file.fileName}`);\n }\n }\n};\n","import type { UniversalFileService } from '../../universalFile/server';\nimport type { AccessPermission, UploadProgress } from '../../universalFile/types';\nimport type { BoothFileKind, CreateBoothUploadInput, CreateBoothUploadResult } from '../types';\nimport { BoothVaultService } from '../core';\nimport { validateUploadFiles } from './security';\n\nexport interface BoothIncomingFile {\n file: File;\n kind?: BoothFileKind;\n}\n\nexport interface CreateBoothUploadWithOSSInput {\n boothId: string;\n files: BoothIncomingFile[];\n metadata?: CreateBoothUploadInput['metadata'];\n ttlHours?: number;\n moduleId?: string;\n businessId?: string;\n permission?: AccessPermission;\n onProgress?: (fileName: string, progress: UploadProgress) => void;\n}\n\nexport const uploadToOSSAndCreateBoothRecord = async (\n params: CreateBoothUploadWithOSSInput,\n deps: {\n fileService: UniversalFileService;\n vaultService: BoothVaultService;\n }\n): Promise<CreateBoothUploadResult> => {\n validateUploadFiles(\n params.files.map((item) => ({ fileName: item.file.name, size: item.file.size })),\n {\n maxFiles: 20,\n maxSingleFileSizeBytes: 2 * 1024 * 1024 * 1024,\n maxTotalSizeBytes: 5 * 1024 * 1024 * 1024,\n }\n );\n\n const moduleId = params.moduleId ?? 'vocaloid-booth';\n const businessId = params.businessId ?? params.boothId;\n const permission = params.permission ?? 'private';\n\n const uploaded = await Promise.all(\n params.files.map(async (item) => {\n const metadata = await deps.fileService.uploadFile(\n {\n file: item.file,\n moduleId,\n businessId,\n permission,\n metadata: {\n boothId: params.boothId,\n kind: item.kind ?? 'other',\n },\n },\n undefined,\n (progress) => params.onProgress?.(item.file.name, progress)\n );\n\n return {\n fileName: metadata.originalName,\n size: metadata.size,\n mimeType: metadata.mimeType,\n checksum: metadata.hash,\n objectKey: metadata.storagePath,\n kind: item.kind ?? 'other',\n };\n })\n );\n\n return deps.vaultService.createUpload({\n boothId: params.boothId,\n ttlHours: params.ttlHours,\n metadata: params.metadata,\n files: uploaded,\n });\n};\n","import type { BoothAuditEvent } from '../types';\n\nexport interface BoothAuditSink {\n log(event: BoothAuditEvent): Promise<void> | void;\n}\n\nexport class InMemoryBoothAuditSink implements BoothAuditSink {\n private readonly events: BoothAuditEvent[] = [];\n\n log(event: BoothAuditEvent): void {\n this.events.push(event);\n }\n\n list(): BoothAuditEvent[] {\n return [...this.events];\n }\n}\n\nexport const createAuditLogger = (sink: BoothAuditSink) => {\n return (event: BoothAuditEvent): void => {\n sink.log(event);\n };\n};\n"]}
@@ -0,0 +1,3 @@
1
+ export { f as BoothAuditEvent, g as BoothAuditEventType, b as BoothFileItem, c as BoothFileKind, h as BoothRedeemGuardLike, a as BoothUploadRecord, d as BoothVaultService, i as BoothVaultServiceOptions, B as BoothVaultStore, C as CreateBoothUploadInput, e as CreateBoothUploadResult } from '../../boothVaultService-Cn4WPhjg.mjs';
2
+ export { BoothConfigPage, BoothConfigPageProps, BoothRedeemPanel, BoothRedeemPanelProps, BoothSuccessCard, BoothSuccessCardProps, BoothUploadPanel, BoothUploadPanelProps, BoothUploadSubmitPayload, GenerateMatchCodeOptions, VocaloidBoothConfig, defaultVocaloidBoothConfig, generateMatchCode, normalizeMatchCode, normalizeVocaloidBoothConfig } from '../index.mjs';
3
+ import 'react';
@@ -0,0 +1,3 @@
1
+ export { f as BoothAuditEvent, g as BoothAuditEventType, b as BoothFileItem, c as BoothFileKind, h as BoothRedeemGuardLike, a as BoothUploadRecord, d as BoothVaultService, i as BoothVaultServiceOptions, B as BoothVaultStore, C as CreateBoothUploadInput, e as CreateBoothUploadResult } from '../../boothVaultService-Cn4WPhjg.js';
2
+ export { BoothConfigPage, BoothConfigPageProps, BoothRedeemPanel, BoothRedeemPanelProps, BoothSuccessCard, BoothSuccessCardProps, BoothUploadPanel, BoothUploadPanelProps, BoothUploadSubmitPayload, GenerateMatchCodeOptions, VocaloidBoothConfig, defaultVocaloidBoothConfig, generateMatchCode, normalizeMatchCode, normalizeVocaloidBoothConfig } from '../index.js';
3
+ import 'react';
@@ -0,0 +1,376 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var React = require('react');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var React__default = /*#__PURE__*/_interopDefault(React);
9
+
10
+ // src/vocaloidBooth/core/code.ts
11
+ var AMBIGUOUS = /* @__PURE__ */ new Set(["0", "1", "I", "O", "L"]);
12
+ var ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789".split("").filter((c) => !AMBIGUOUS.has(c));
13
+ var normalizeMatchCode = (value) => value.trim().toUpperCase();
14
+ var generateMatchCode = async ({
15
+ length = 6,
16
+ maxAttempts = 20,
17
+ exists
18
+ }) => {
19
+ if (length < 4) {
20
+ throw new Error("Match code length must be at least 4");
21
+ }
22
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
23
+ const code = Array.from({ length }).map(() => ALPHABET[Math.floor(Math.random() * ALPHABET.length)]).join("");
24
+ if (!await exists(code)) {
25
+ return code;
26
+ }
27
+ }
28
+ throw new Error("Unable to generate unique match code");
29
+ };
30
+ var BoothVaultService = class {
31
+ emitAudit(event) {
32
+ this.onAuditEvent?.({
33
+ ...event,
34
+ at: (/* @__PURE__ */ new Date()).toISOString()
35
+ });
36
+ }
37
+ constructor(options) {
38
+ this.store = options.store;
39
+ this.codeLength = options.codeLength ?? 6;
40
+ this.defaultTtlHours = options.defaultTtlHours ?? 24 * 14;
41
+ this.baseDownloadPath = options.baseDownloadPath ?? "/redeem";
42
+ this.redeemGuard = options.redeemGuard;
43
+ this.onAuditEvent = options.onAuditEvent;
44
+ }
45
+ async createUpload(input) {
46
+ if (!input.files?.length) {
47
+ throw new Error("At least one file is required");
48
+ }
49
+ const now = /* @__PURE__ */ new Date();
50
+ const ttlHours = Math.max(1, input.ttlHours ?? this.defaultTtlHours);
51
+ const expiresAt = new Date(now.getTime() + ttlHours * 60 * 60 * 1e3);
52
+ const matchCode = await generateMatchCode({
53
+ length: this.codeLength,
54
+ exists: (code) => this.store.existsByMatchCode(code)
55
+ });
56
+ const record = {
57
+ id: crypto.randomUUID(),
58
+ boothId: input.boothId,
59
+ matchCode,
60
+ createdAt: now.toISOString(),
61
+ expiresAt: expiresAt.toISOString(),
62
+ files: input.files.map((file) => ({
63
+ ...file,
64
+ id: crypto.randomUUID()
65
+ })),
66
+ metadata: input.metadata,
67
+ status: "active",
68
+ downloadCount: 0
69
+ };
70
+ await this.store.saveRecord(record);
71
+ this.emitAudit({
72
+ type: "upload.created",
73
+ boothId: record.boothId,
74
+ recordId: record.id,
75
+ matchCode: record.matchCode,
76
+ detail: { fileCount: record.files.length }
77
+ });
78
+ return {
79
+ record,
80
+ downloadUrlPath: `${this.baseDownloadPath}?code=${record.matchCode}`
81
+ };
82
+ }
83
+ async getByMatchCode(matchCode) {
84
+ const normalized = normalizeMatchCode(matchCode);
85
+ const record = await this.store.findByMatchCode(normalized);
86
+ if (!record) {
87
+ return null;
88
+ }
89
+ if (new Date(record.expiresAt).getTime() <= Date.now() && record.status === "active") {
90
+ return {
91
+ ...record,
92
+ status: "expired"
93
+ };
94
+ }
95
+ return record;
96
+ }
97
+ async markDownloaded(recordId) {
98
+ await this.store.incrementDownloadCount(recordId);
99
+ }
100
+ async resolveDownloadFilesByCode(matchCode, options) {
101
+ const requesterKey = options?.requesterKey;
102
+ if (requesterKey && this.redeemGuard) {
103
+ try {
104
+ this.redeemGuard.assertAllowed(requesterKey);
105
+ } catch (error) {
106
+ this.emitAudit({
107
+ type: "redeem.blocked",
108
+ requesterKey,
109
+ matchCode,
110
+ detail: { message: error instanceof Error ? error.message : "blocked" }
111
+ });
112
+ throw error;
113
+ }
114
+ }
115
+ const record = await this.getByMatchCode(matchCode);
116
+ const success = !!record && record.status === "active";
117
+ if (requesterKey && this.redeemGuard) {
118
+ this.redeemGuard.registerAttempt(requesterKey, success);
119
+ }
120
+ if (!success) {
121
+ this.emitAudit({
122
+ type: "redeem.failed",
123
+ requesterKey,
124
+ matchCode,
125
+ boothId: record?.boothId,
126
+ recordId: record?.id
127
+ });
128
+ return record;
129
+ }
130
+ await this.markDownloaded(record.id);
131
+ const reloaded = this.store.findByRecordId ? await this.store.findByRecordId(record.id) : await this.getByMatchCode(record.matchCode);
132
+ this.emitAudit({
133
+ type: "redeem.success",
134
+ requesterKey,
135
+ matchCode: record.matchCode,
136
+ boothId: record.boothId,
137
+ recordId: record.id
138
+ });
139
+ return reloaded ?? record;
140
+ }
141
+ };
142
+
143
+ // src/vocaloidBooth/core/config.ts
144
+ var defaultVocaloidBoothConfig = {
145
+ boothId: "default-booth",
146
+ title: "MMD / Vocaloid \u521B\u4F5C\u6587\u4EF6\u5BC4\u5B58\u7AD9",
147
+ description: "\u4E0A\u4F20\u521B\u4F5C\u6587\u4EF6\u5E76\u751F\u6210\u5339\u914D\u7801\uFF0C\u540E\u7EED\u53EF\u51ED\u7801\u4E0B\u8F7D",
148
+ defaultTtlHours: 24 * 14,
149
+ maxFiles: 20,
150
+ maxSingleFileSizeMb: 2048,
151
+ maxTotalFileSizeMb: 5120,
152
+ allowedExtensions: ["zip", "7z", "rar", "vsqx", "vpr", "vmd", "pmx", "wav", "mp3", "mp4"]
153
+ };
154
+ var normalizeVocaloidBoothConfig = (input) => {
155
+ const merged = {
156
+ ...defaultVocaloidBoothConfig,
157
+ ...input ?? {}
158
+ };
159
+ return {
160
+ ...merged,
161
+ boothId: merged.boothId || defaultVocaloidBoothConfig.boothId,
162
+ title: merged.title || defaultVocaloidBoothConfig.title,
163
+ defaultTtlHours: Math.max(1, merged.defaultTtlHours),
164
+ maxFiles: Math.max(1, merged.maxFiles),
165
+ maxSingleFileSizeMb: Math.max(1, merged.maxSingleFileSizeMb),
166
+ maxTotalFileSizeMb: Math.max(1, merged.maxTotalFileSizeMb),
167
+ allowedExtensions: (merged.allowedExtensions?.length ? merged.allowedExtensions : defaultVocaloidBoothConfig.allowedExtensions).map((ext) => ext.toLowerCase())
168
+ };
169
+ };
170
+ var BoothUploadPanel = ({
171
+ boothId,
172
+ maxFiles = 10,
173
+ maxFileSizeMb = 2048,
174
+ accept,
175
+ uploading = false,
176
+ onSubmit
177
+ }) => {
178
+ const [files, setFiles] = React.useState([]);
179
+ const [nickname, setNickname] = React.useState("");
180
+ const [contactTail, setContactTail] = React.useState("");
181
+ const [ttlHours, setTtlHours] = React.useState(24 * 14);
182
+ const [error, setError] = React.useState(null);
183
+ const totalSizeMb = React.useMemo(
184
+ () => files.reduce((acc, file) => acc + file.size, 0) / 1024 / 1024,
185
+ [files]
186
+ );
187
+ const addFiles = (newFiles) => {
188
+ if (!newFiles) return;
189
+ const incoming = Array.from(newFiles);
190
+ const next = [...files, ...incoming];
191
+ if (next.length > maxFiles) {
192
+ setError(`\u6700\u591A\u4E0A\u4F20 ${maxFiles} \u4E2A\u6587\u4EF6`);
193
+ return;
194
+ }
195
+ const oversized = incoming.find((f) => f.size > maxFileSizeMb * 1024 * 1024);
196
+ if (oversized) {
197
+ setError(`\u6587\u4EF6 ${oversized.name} \u8D85\u8FC7 ${maxFileSizeMb}MB \u9650\u5236`);
198
+ return;
199
+ }
200
+ setError(null);
201
+ setFiles(next);
202
+ };
203
+ const removeFile = (name) => setFiles((prev) => prev.filter((f) => f.name !== name));
204
+ const handleSubmit = async () => {
205
+ if (files.length === 0) {
206
+ setError("\u8BF7\u5148\u9009\u62E9\u81F3\u5C11\u4E00\u4E2A\u6587\u4EF6");
207
+ return;
208
+ }
209
+ setError(null);
210
+ await onSubmit({
211
+ boothId,
212
+ files,
213
+ nickname: nickname || void 0,
214
+ contactTail: contactTail || void 0,
215
+ ttlHours
216
+ });
217
+ };
218
+ return /* @__PURE__ */ React__default.default.createElement("div", { className: "rounded-xl border border-slate-200 bg-white p-4 shadow-sm" }, /* @__PURE__ */ React__default.default.createElement("h3", { className: "mb-3 text-lg font-semibold" }, "\u4E0A\u4F20\u521B\u4F5C\u6587\u4EF6"), /* @__PURE__ */ React__default.default.createElement(
219
+ "input",
220
+ {
221
+ type: "file",
222
+ multiple: true,
223
+ accept,
224
+ onChange: (e) => addFiles(e.target.files),
225
+ className: "mb-3 block w-full text-sm"
226
+ }
227
+ ), /* @__PURE__ */ React__default.default.createElement("div", { className: "mb-3 grid grid-cols-1 gap-2 md:grid-cols-3" }, /* @__PURE__ */ React__default.default.createElement(
228
+ "input",
229
+ {
230
+ value: nickname,
231
+ onChange: (e) => setNickname(e.target.value),
232
+ placeholder: "\u6635\u79F0\uFF08\u53EF\u9009\uFF09",
233
+ className: "rounded-md border px-3 py-2 text-sm"
234
+ }
235
+ ), /* @__PURE__ */ React__default.default.createElement(
236
+ "input",
237
+ {
238
+ value: contactTail,
239
+ onChange: (e) => setContactTail(e.target.value),
240
+ placeholder: "\u8054\u7CFB\u65B9\u5F0F\u540E4\u4F4D\uFF08\u53EF\u9009\uFF09",
241
+ className: "rounded-md border px-3 py-2 text-sm"
242
+ }
243
+ ), /* @__PURE__ */ React__default.default.createElement(
244
+ "input",
245
+ {
246
+ value: ttlHours,
247
+ type: "number",
248
+ min: 1,
249
+ onChange: (e) => setTtlHours(Number(e.target.value) || 24),
250
+ placeholder: "\u4FDD\u5B58\u65F6\u957F\uFF08\u5C0F\u65F6\uFF09",
251
+ className: "rounded-md border px-3 py-2 text-sm"
252
+ }
253
+ )), /* @__PURE__ */ React__default.default.createElement("div", { className: "mb-3 text-xs text-slate-500" }, "\u5DF2\u9009 ", files.length, " \u4E2A\u6587\u4EF6\uFF0C\u603B\u8BA1 ", totalSizeMb.toFixed(2), " MB"), /* @__PURE__ */ React__default.default.createElement("ul", { className: "mb-3 max-h-40 overflow-auto rounded-md border border-slate-100 p-2 text-sm" }, files.length === 0 && /* @__PURE__ */ React__default.default.createElement("li", { className: "text-slate-400" }, "\u5C1A\u672A\u9009\u62E9\u6587\u4EF6"), files.map((file) => /* @__PURE__ */ React__default.default.createElement("li", { key: `${file.name}-${file.size}`, className: "mb-1 flex items-center justify-between gap-2" }, /* @__PURE__ */ React__default.default.createElement("span", { className: "truncate" }, file.name), /* @__PURE__ */ React__default.default.createElement("button", { type: "button", className: "text-rose-500", onClick: () => removeFile(file.name) }, "\u79FB\u9664")))), error && /* @__PURE__ */ React__default.default.createElement("div", { className: "mb-3 rounded-md bg-rose-50 p-2 text-sm text-rose-700" }, error), /* @__PURE__ */ React__default.default.createElement(
254
+ "button",
255
+ {
256
+ type: "button",
257
+ disabled: uploading,
258
+ onClick: handleSubmit,
259
+ className: "rounded-md bg-indigo-600 px-3 py-2 text-white disabled:cursor-not-allowed disabled:opacity-50"
260
+ },
261
+ uploading ? "\u4E0A\u4F20\u4E2D..." : "\u5F00\u59CB\u4E0A\u4F20"
262
+ ));
263
+ };
264
+ var BoothRedeemPanel = ({ onRedeem, loading }) => {
265
+ const [matchCode, setMatchCode] = React.useState("");
266
+ const [record, setRecord] = React.useState(null);
267
+ const [error, setError] = React.useState(null);
268
+ const handleRedeem = async () => {
269
+ setError(null);
270
+ const result = await onRedeem(matchCode.trim());
271
+ if (!result) {
272
+ setRecord(null);
273
+ setError("\u5339\u914D\u7801\u4E0D\u5B58\u5728\uFF0C\u8BF7\u68C0\u67E5\u540E\u91CD\u8BD5");
274
+ return;
275
+ }
276
+ if (result.status !== "active") {
277
+ setRecord(result);
278
+ setError("\u5339\u914D\u7801\u5DF2\u8FC7\u671F\u6216\u5931\u6548");
279
+ return;
280
+ }
281
+ setRecord(result);
282
+ };
283
+ return /* @__PURE__ */ React__default.default.createElement("div", { className: "rounded-xl border border-slate-200 bg-white p-4 shadow-sm" }, /* @__PURE__ */ React__default.default.createElement("h3", { className: "mb-3 text-lg font-semibold" }, "\u51ED\u5339\u914D\u7801\u4E0B\u8F7D"), /* @__PURE__ */ React__default.default.createElement("div", { className: "mb-3 flex gap-2" }, /* @__PURE__ */ React__default.default.createElement(
284
+ "input",
285
+ {
286
+ value: matchCode,
287
+ onChange: (e) => setMatchCode(e.target.value.toUpperCase()),
288
+ placeholder: "\u8F93\u5165\u5339\u914D\u7801\uFF08\u5982 A7K9Q2\uFF09",
289
+ className: "w-full rounded-md border px-3 py-2 text-sm uppercase"
290
+ }
291
+ ), /* @__PURE__ */ React__default.default.createElement(
292
+ "button",
293
+ {
294
+ type: "button",
295
+ onClick: handleRedeem,
296
+ disabled: loading,
297
+ className: "rounded-md bg-slate-900 px-3 py-2 text-white disabled:opacity-50"
298
+ },
299
+ "\u67E5\u8BE2"
300
+ )), error && /* @__PURE__ */ React__default.default.createElement("div", { className: "mb-3 rounded-md bg-amber-50 p-2 text-sm text-amber-700" }, error), record && /* @__PURE__ */ React__default.default.createElement("div", { className: "rounded-md border border-slate-100 p-3" }, /* @__PURE__ */ React__default.default.createElement("div", { className: "mb-2 text-xs text-slate-500" }, "\u5171 ", record.files.length, " \u4E2A\u6587\u4EF6"), /* @__PURE__ */ React__default.default.createElement("ul", { className: "space-y-1 text-sm" }, record.files.map((file) => /* @__PURE__ */ React__default.default.createElement("li", { key: file.id, className: "flex items-center justify-between gap-2" }, /* @__PURE__ */ React__default.default.createElement("span", { className: "truncate" }, file.fileName), /* @__PURE__ */ React__default.default.createElement("a", { href: file.objectKey, className: "text-indigo-600 hover:underline", download: true }, "\u4E0B\u8F7D"))))));
301
+ };
302
+ var BoothSuccessCard = ({
303
+ matchCode,
304
+ expiresAt,
305
+ downloadUrlPath,
306
+ onCopyCode,
307
+ className
308
+ }) => {
309
+ const handleCopy = async () => {
310
+ try {
311
+ await navigator.clipboard.writeText(matchCode);
312
+ } catch {
313
+ }
314
+ onCopyCode?.(matchCode);
315
+ };
316
+ return /* @__PURE__ */ React__default.default.createElement("div", { className: `rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm ${className ?? ""}` }, /* @__PURE__ */ React__default.default.createElement("div", { className: "mb-2 text-xs text-emerald-800" }, "\u4E0A\u4F20\u5B8C\u6210\uFF0C\u5DF2\u751F\u6210\u5339\u914D\u7801"), /* @__PURE__ */ React__default.default.createElement("div", { className: "mb-3 text-2xl font-bold tracking-widest text-emerald-900" }, matchCode), /* @__PURE__ */ React__default.default.createElement("div", { className: "mb-3 text-xs text-emerald-800" }, "\u8FC7\u671F\u65F6\u95F4\uFF1A", new Date(expiresAt).toLocaleString()), /* @__PURE__ */ React__default.default.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ React__default.default.createElement(
317
+ "button",
318
+ {
319
+ type: "button",
320
+ onClick: handleCopy,
321
+ className: "rounded-md bg-emerald-600 px-3 py-2 text-white hover:bg-emerald-700"
322
+ },
323
+ "\u590D\u5236\u5339\u914D\u7801"
324
+ ), /* @__PURE__ */ React__default.default.createElement(
325
+ "a",
326
+ {
327
+ href: downloadUrlPath,
328
+ className: "rounded-md border border-emerald-400 bg-white px-3 py-2 text-emerald-700 hover:bg-emerald-100"
329
+ },
330
+ "\u6253\u5F00\u4E0B\u8F7D\u9875"
331
+ )));
332
+ };
333
+ var BoothConfigPage = ({ initialConfig, onSave }) => {
334
+ const [config, setConfig] = React.useState(
335
+ normalizeVocaloidBoothConfig(initialConfig)
336
+ );
337
+ const [saving, setSaving] = React.useState(false);
338
+ const extText = React.useMemo(() => config.allowedExtensions.join(","), [config.allowedExtensions]);
339
+ const update = (key, value) => setConfig((prev) => ({ ...prev, [key]: value }));
340
+ const save = async () => {
341
+ setSaving(true);
342
+ try {
343
+ const normalized = normalizeVocaloidBoothConfig(config);
344
+ setConfig(normalized);
345
+ await onSave?.(normalized);
346
+ } finally {
347
+ setSaving(false);
348
+ }
349
+ };
350
+ const reset = () => setConfig(defaultVocaloidBoothConfig);
351
+ return /* @__PURE__ */ React__default.default.createElement("div", { className: "rounded-xl border border-slate-200 bg-white p-4 shadow-sm space-y-3" }, /* @__PURE__ */ React__default.default.createElement("h3", { className: "text-lg font-semibold" }, "Vocaloid Booth \u914D\u7F6E\u9875"), /* @__PURE__ */ React__default.default.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm" }, /* @__PURE__ */ React__default.default.createElement("input", { className: "rounded border px-3 py-2", value: config.boothId, onChange: (e) => update("boothId", e.target.value), placeholder: "boothId" }), /* @__PURE__ */ React__default.default.createElement("input", { className: "rounded border px-3 py-2", value: config.title, onChange: (e) => update("title", e.target.value), placeholder: "\u6807\u9898" }), /* @__PURE__ */ React__default.default.createElement("input", { className: "rounded border px-3 py-2 md:col-span-2", value: config.description ?? "", onChange: (e) => update("description", e.target.value), placeholder: "\u63CF\u8FF0" }), /* @__PURE__ */ React__default.default.createElement("input", { className: "rounded border px-3 py-2", type: "number", value: config.defaultTtlHours, onChange: (e) => update("defaultTtlHours", Number(e.target.value) || 1), placeholder: "\u9ED8\u8BA4\u4FDD\u5B58\u65F6\u957F\uFF08\u5C0F\u65F6\uFF09" }), /* @__PURE__ */ React__default.default.createElement("input", { className: "rounded border px-3 py-2", type: "number", value: config.maxFiles, onChange: (e) => update("maxFiles", Number(e.target.value) || 1), placeholder: "\u6700\u5927\u6587\u4EF6\u6570" }), /* @__PURE__ */ React__default.default.createElement("input", { className: "rounded border px-3 py-2", type: "number", value: config.maxSingleFileSizeMb, onChange: (e) => update("maxSingleFileSizeMb", Number(e.target.value) || 1), placeholder: "\u5355\u6587\u4EF6\u4E0A\u9650 MB" }), /* @__PURE__ */ React__default.default.createElement("input", { className: "rounded border px-3 py-2", type: "number", value: config.maxTotalFileSizeMb, onChange: (e) => update("maxTotalFileSizeMb", Number(e.target.value) || 1), placeholder: "\u603B\u5927\u5C0F\u4E0A\u9650 MB" }), /* @__PURE__ */ React__default.default.createElement(
352
+ "textarea",
353
+ {
354
+ className: "rounded border px-3 py-2 md:col-span-2",
355
+ rows: 3,
356
+ value: extText,
357
+ onChange: (e) => update(
358
+ "allowedExtensions",
359
+ e.target.value.split(",").map((v) => v.trim()).filter(Boolean)
360
+ ),
361
+ placeholder: "\u5141\u8BB8\u540E\u7F00\uFF0C\u9017\u53F7\u5206\u9694"
362
+ }
363
+ )), /* @__PURE__ */ React__default.default.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ React__default.default.createElement("button", { className: "rounded bg-indigo-600 px-3 py-2 text-white", disabled: saving, onClick: save }, saving ? "\u4FDD\u5B58\u4E2D..." : "\u4FDD\u5B58\u914D\u7F6E"), /* @__PURE__ */ React__default.default.createElement("button", { className: "rounded border px-3 py-2", onClick: reset }, "\u6062\u590D\u9ED8\u8BA4")));
364
+ };
365
+
366
+ exports.BoothConfigPage = BoothConfigPage;
367
+ exports.BoothRedeemPanel = BoothRedeemPanel;
368
+ exports.BoothSuccessCard = BoothSuccessCard;
369
+ exports.BoothUploadPanel = BoothUploadPanel;
370
+ exports.BoothVaultService = BoothVaultService;
371
+ exports.defaultVocaloidBoothConfig = defaultVocaloidBoothConfig;
372
+ exports.generateMatchCode = generateMatchCode;
373
+ exports.normalizeMatchCode = normalizeMatchCode;
374
+ exports.normalizeVocaloidBoothConfig = normalizeVocaloidBoothConfig;
375
+ //# sourceMappingURL=index.js.map
376
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/vocaloidBooth/core/code.ts","../../../src/vocaloidBooth/core/boothVaultService.ts","../../../src/vocaloidBooth/core/config.ts","../../../src/vocaloidBooth/components/BoothUploadPanel.tsx","../../../src/vocaloidBooth/components/BoothRedeemPanel.tsx","../../../src/vocaloidBooth/components/BoothSuccessCard.tsx","../../../src/vocaloidBooth/components/BoothConfigPage.tsx"],"names":["randomUUID","useState","useMemo","React"],"mappings":"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","file":"index.js","sourcesContent":["const AMBIGUOUS = new Set(['0', '1', 'I', 'O', 'L']);\nconst ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'.split('').filter((c) => !AMBIGUOUS.has(c));\n\nexport interface GenerateMatchCodeOptions {\n length?: number;\n maxAttempts?: number;\n exists: (code: string) => Promise<boolean>;\n}\n\nexport const normalizeMatchCode = (value: string): string => value.trim().toUpperCase();\n\nexport const generateMatchCode = async ({\n length = 6,\n maxAttempts = 20,\n exists,\n}: GenerateMatchCodeOptions): Promise<string> => {\n if (length < 4) {\n throw new Error('Match code length must be at least 4');\n }\n\n for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n const code = Array.from({ length })\n .map(() => ALPHABET[Math.floor(Math.random() * ALPHABET.length)])\n .join('');\n\n // eslint-disable-next-line no-await-in-loop\n if (!(await exists(code))) {\n return code;\n }\n }\n\n throw new Error('Unable to generate unique match code');\n};\n","import { randomUUID } from 'crypto';\nimport type {\n BoothAuditEvent,\n BoothUploadRecord,\n BoothVaultStore,\n CreateBoothUploadInput,\n CreateBoothUploadResult,\n} from '../types';\nimport { generateMatchCode, normalizeMatchCode } from './code';\n\nexport interface BoothRedeemGuardLike {\n assertAllowed(subjectKey: string): void;\n registerAttempt(subjectKey: string, success: boolean): void;\n}\n\nexport interface BoothVaultServiceOptions {\n store: BoothVaultStore;\n codeLength?: number;\n defaultTtlHours?: number;\n baseDownloadPath?: string;\n redeemGuard?: BoothRedeemGuardLike;\n onAuditEvent?: (event: BoothAuditEvent) => void;\n}\n\nexport class BoothVaultService {\n private emitAudit(event: Omit<BoothAuditEvent, 'at'>): void {\n this.onAuditEvent?.({\n ...event,\n at: new Date().toISOString(),\n });\n }\n private readonly store: BoothVaultStore;\n private readonly codeLength: number;\n private readonly defaultTtlHours: number;\n private readonly baseDownloadPath: string;\n private readonly redeemGuard?: BoothRedeemGuardLike;\n private readonly onAuditEvent?: (event: BoothAuditEvent) => void;\n\n constructor(options: BoothVaultServiceOptions) {\n this.store = options.store;\n this.codeLength = options.codeLength ?? 6;\n this.defaultTtlHours = options.defaultTtlHours ?? 24 * 14;\n this.baseDownloadPath = options.baseDownloadPath ?? '/redeem';\n this.redeemGuard = options.redeemGuard;\n this.onAuditEvent = options.onAuditEvent;\n }\n\n async createUpload(input: CreateBoothUploadInput): Promise<CreateBoothUploadResult> {\n if (!input.files?.length) {\n throw new Error('At least one file is required');\n }\n\n const now = new Date();\n const ttlHours = Math.max(1, input.ttlHours ?? this.defaultTtlHours);\n const expiresAt = new Date(now.getTime() + ttlHours * 60 * 60 * 1000);\n\n const matchCode = await generateMatchCode({\n length: this.codeLength,\n exists: (code) => this.store.existsByMatchCode(code),\n });\n\n const record: BoothUploadRecord = {\n id: randomUUID(),\n boothId: input.boothId,\n matchCode,\n createdAt: now.toISOString(),\n expiresAt: expiresAt.toISOString(),\n files: input.files.map((file) => ({\n ...file,\n id: randomUUID(),\n })),\n metadata: input.metadata,\n status: 'active',\n downloadCount: 0,\n };\n\n await this.store.saveRecord(record);\n this.emitAudit({\n type: 'upload.created',\n boothId: record.boothId,\n recordId: record.id,\n matchCode: record.matchCode,\n detail: { fileCount: record.files.length },\n });\n\n return {\n record,\n downloadUrlPath: `${this.baseDownloadPath}?code=${record.matchCode}`,\n };\n }\n\n async getByMatchCode(matchCode: string): Promise<BoothUploadRecord | null> {\n const normalized = normalizeMatchCode(matchCode);\n const record = await this.store.findByMatchCode(normalized);\n\n if (!record) {\n return null;\n }\n\n if (new Date(record.expiresAt).getTime() <= Date.now() && record.status === 'active') {\n return {\n ...record,\n status: 'expired',\n };\n }\n\n return record;\n }\n\n async markDownloaded(recordId: string): Promise<void> {\n await this.store.incrementDownloadCount(recordId);\n }\n\n async resolveDownloadFilesByCode(\n matchCode: string,\n options?: { requesterKey?: string }\n ): Promise<BoothUploadRecord | null> {\n const requesterKey = options?.requesterKey;\n if (requesterKey && this.redeemGuard) {\n try {\n this.redeemGuard.assertAllowed(requesterKey);\n } catch (error) {\n this.emitAudit({\n type: 'redeem.blocked',\n requesterKey,\n matchCode,\n detail: { message: error instanceof Error ? error.message : 'blocked' },\n });\n throw error;\n }\n }\n\n const record = await this.getByMatchCode(matchCode);\n const success = !!record && record.status === 'active';\n\n if (requesterKey && this.redeemGuard) {\n this.redeemGuard.registerAttempt(requesterKey, success);\n }\n\n if (!success) {\n this.emitAudit({\n type: 'redeem.failed',\n requesterKey,\n matchCode,\n boothId: record?.boothId,\n recordId: record?.id,\n });\n return record;\n }\n\n await this.markDownloaded(record.id);\n const reloaded = this.store.findByRecordId\n ? await this.store.findByRecordId(record.id)\n : await this.getByMatchCode(record.matchCode);\n\n this.emitAudit({\n type: 'redeem.success',\n requesterKey,\n matchCode: record.matchCode,\n boothId: record.boothId,\n recordId: record.id,\n });\n\n return reloaded ?? record;\n }\n}\n","export interface VocaloidBoothConfig {\n boothId: string;\n title: string;\n description?: string;\n defaultTtlHours: number;\n maxFiles: number;\n maxSingleFileSizeMb: number;\n maxTotalFileSizeMb: number;\n allowedExtensions: string[];\n}\n\nexport const defaultVocaloidBoothConfig: VocaloidBoothConfig = {\n boothId: 'default-booth',\n title: 'MMD / Vocaloid 创作文件寄存站',\n description: '上传创作文件并生成匹配码,后续可凭码下载',\n defaultTtlHours: 24 * 14,\n maxFiles: 20,\n maxSingleFileSizeMb: 2048,\n maxTotalFileSizeMb: 5120,\n allowedExtensions: ['zip', '7z', 'rar', 'vsqx', 'vpr', 'vmd', 'pmx', 'wav', 'mp3', 'mp4'],\n};\n\nexport const normalizeVocaloidBoothConfig = (\n input?: Partial<VocaloidBoothConfig>\n): VocaloidBoothConfig => {\n const merged = {\n ...defaultVocaloidBoothConfig,\n ...(input ?? {}),\n };\n\n return {\n ...merged,\n boothId: merged.boothId || defaultVocaloidBoothConfig.boothId,\n title: merged.title || defaultVocaloidBoothConfig.title,\n defaultTtlHours: Math.max(1, merged.defaultTtlHours),\n maxFiles: Math.max(1, merged.maxFiles),\n maxSingleFileSizeMb: Math.max(1, merged.maxSingleFileSizeMb),\n maxTotalFileSizeMb: Math.max(1, merged.maxTotalFileSizeMb),\n allowedExtensions: (merged.allowedExtensions?.length\n ? merged.allowedExtensions\n : defaultVocaloidBoothConfig.allowedExtensions\n ).map((ext) => ext.toLowerCase()),\n };\n};\n","'use client';\n\nimport React, { useMemo, useState } from 'react';\n\nexport interface BoothUploadSubmitPayload {\n boothId: string;\n files: File[];\n nickname?: string;\n contactTail?: string;\n ttlHours?: number;\n}\n\nexport interface BoothUploadPanelProps {\n boothId: string;\n maxFiles?: number;\n maxFileSizeMb?: number;\n accept?: string;\n uploading?: boolean;\n onSubmit: (payload: BoothUploadSubmitPayload) => Promise<void> | void;\n}\n\nexport const BoothUploadPanel: React.FC<BoothUploadPanelProps> = ({\n boothId,\n maxFiles = 10,\n maxFileSizeMb = 2048,\n accept,\n uploading = false,\n onSubmit,\n}) => {\n const [files, setFiles] = useState<File[]>([]);\n const [nickname, setNickname] = useState('');\n const [contactTail, setContactTail] = useState('');\n const [ttlHours, setTtlHours] = useState(24 * 14);\n const [error, setError] = useState<string | null>(null);\n\n const totalSizeMb = useMemo(\n () => files.reduce((acc, file) => acc + file.size, 0) / 1024 / 1024,\n [files]\n );\n\n const addFiles = (newFiles: FileList | null) => {\n if (!newFiles) return;\n const incoming = Array.from(newFiles);\n const next = [...files, ...incoming];\n\n if (next.length > maxFiles) {\n setError(`最多上传 ${maxFiles} 个文件`);\n return;\n }\n\n const oversized = incoming.find((f) => f.size > maxFileSizeMb * 1024 * 1024);\n if (oversized) {\n setError(`文件 ${oversized.name} 超过 ${maxFileSizeMb}MB 限制`);\n return;\n }\n\n setError(null);\n setFiles(next);\n };\n\n const removeFile = (name: string) => setFiles((prev) => prev.filter((f) => f.name !== name));\n\n const handleSubmit = async () => {\n if (files.length === 0) {\n setError('请先选择至少一个文件');\n return;\n }\n setError(null);\n await onSubmit({\n boothId,\n files,\n nickname: nickname || undefined,\n contactTail: contactTail || undefined,\n ttlHours,\n });\n };\n\n return (\n <div className=\"rounded-xl border border-slate-200 bg-white p-4 shadow-sm\">\n <h3 className=\"mb-3 text-lg font-semibold\">上传创作文件</h3>\n\n <input\n type=\"file\"\n multiple\n accept={accept}\n onChange={(e) => addFiles(e.target.files)}\n className=\"mb-3 block w-full text-sm\"\n />\n\n <div className=\"mb-3 grid grid-cols-1 gap-2 md:grid-cols-3\">\n <input\n value={nickname}\n onChange={(e) => setNickname(e.target.value)}\n placeholder=\"昵称(可选)\"\n className=\"rounded-md border px-3 py-2 text-sm\"\n />\n <input\n value={contactTail}\n onChange={(e) => setContactTail(e.target.value)}\n placeholder=\"联系方式后4位(可选)\"\n className=\"rounded-md border px-3 py-2 text-sm\"\n />\n <input\n value={ttlHours}\n type=\"number\"\n min={1}\n onChange={(e) => setTtlHours(Number(e.target.value) || 24)}\n placeholder=\"保存时长(小时)\"\n className=\"rounded-md border px-3 py-2 text-sm\"\n />\n </div>\n\n <div className=\"mb-3 text-xs text-slate-500\">\n 已选 {files.length} 个文件,总计 {totalSizeMb.toFixed(2)} MB\n </div>\n\n <ul className=\"mb-3 max-h-40 overflow-auto rounded-md border border-slate-100 p-2 text-sm\">\n {files.length === 0 && <li className=\"text-slate-400\">尚未选择文件</li>}\n {files.map((file) => (\n <li key={`${file.name}-${file.size}`} className=\"mb-1 flex items-center justify-between gap-2\">\n <span className=\"truncate\">{file.name}</span>\n <button type=\"button\" className=\"text-rose-500\" onClick={() => removeFile(file.name)}>\n 移除\n </button>\n </li>\n ))}\n </ul>\n\n {error && <div className=\"mb-3 rounded-md bg-rose-50 p-2 text-sm text-rose-700\">{error}</div>}\n\n <button\n type=\"button\"\n disabled={uploading}\n onClick={handleSubmit}\n className=\"rounded-md bg-indigo-600 px-3 py-2 text-white disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {uploading ? '上传中...' : '开始上传'}\n </button>\n </div>\n );\n};\n","'use client';\n\nimport React, { useState } from 'react';\nimport type { BoothUploadRecord } from '../types';\n\nexport interface BoothRedeemPanelProps {\n loading?: boolean;\n onRedeem: (matchCode: string) => Promise<BoothUploadRecord | null>;\n}\n\nexport const BoothRedeemPanel: React.FC<BoothRedeemPanelProps> = ({ onRedeem, loading }) => {\n const [matchCode, setMatchCode] = useState('');\n const [record, setRecord] = useState<BoothUploadRecord | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const handleRedeem = async () => {\n setError(null);\n const result = await onRedeem(matchCode.trim());\n\n if (!result) {\n setRecord(null);\n setError('匹配码不存在,请检查后重试');\n return;\n }\n\n if (result.status !== 'active') {\n setRecord(result);\n setError('匹配码已过期或失效');\n return;\n }\n\n setRecord(result);\n };\n\n return (\n <div className=\"rounded-xl border border-slate-200 bg-white p-4 shadow-sm\">\n <h3 className=\"mb-3 text-lg font-semibold\">凭匹配码下载</h3>\n\n <div className=\"mb-3 flex gap-2\">\n <input\n value={matchCode}\n onChange={(e) => setMatchCode(e.target.value.toUpperCase())}\n placeholder=\"输入匹配码(如 A7K9Q2)\"\n className=\"w-full rounded-md border px-3 py-2 text-sm uppercase\"\n />\n <button\n type=\"button\"\n onClick={handleRedeem}\n disabled={loading}\n className=\"rounded-md bg-slate-900 px-3 py-2 text-white disabled:opacity-50\"\n >\n 查询\n </button>\n </div>\n\n {error && <div className=\"mb-3 rounded-md bg-amber-50 p-2 text-sm text-amber-700\">{error}</div>}\n\n {record && (\n <div className=\"rounded-md border border-slate-100 p-3\">\n <div className=\"mb-2 text-xs text-slate-500\">共 {record.files.length} 个文件</div>\n <ul className=\"space-y-1 text-sm\">\n {record.files.map((file) => (\n <li key={file.id} className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate\">{file.fileName}</span>\n <a href={file.objectKey} className=\"text-indigo-600 hover:underline\" download>\n 下载\n </a>\n </li>\n ))}\n </ul>\n </div>\n )}\n </div>\n );\n};\n","'use client';\n\nimport React from 'react';\n\nexport interface BoothSuccessCardProps {\n matchCode: string;\n expiresAt: string;\n downloadUrlPath: string;\n onCopyCode?: (code: string) => void;\n className?: string;\n}\n\nexport const BoothSuccessCard: React.FC<BoothSuccessCardProps> = ({\n matchCode,\n expiresAt,\n downloadUrlPath,\n onCopyCode,\n className,\n}) => {\n const handleCopy = async () => {\n try {\n await navigator.clipboard.writeText(matchCode);\n } catch {\n // ignore clipboard errors in non-secure contexts\n }\n onCopyCode?.(matchCode);\n };\n\n return (\n <div className={`rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm ${className ?? ''}`}>\n <div className=\"mb-2 text-xs text-emerald-800\">上传完成,已生成匹配码</div>\n <div className=\"mb-3 text-2xl font-bold tracking-widest text-emerald-900\">{matchCode}</div>\n <div className=\"mb-3 text-xs text-emerald-800\">过期时间:{new Date(expiresAt).toLocaleString()}</div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"rounded-md bg-emerald-600 px-3 py-2 text-white hover:bg-emerald-700\"\n >\n 复制匹配码\n </button>\n <a\n href={downloadUrlPath}\n className=\"rounded-md border border-emerald-400 bg-white px-3 py-2 text-emerald-700 hover:bg-emerald-100\"\n >\n 打开下载页\n </a>\n </div>\n </div>\n );\n};\n","'use client';\n\nimport React, { useMemo, useState } from 'react';\nimport {\n defaultVocaloidBoothConfig,\n normalizeVocaloidBoothConfig,\n type VocaloidBoothConfig,\n} from '../core';\n\nexport interface BoothConfigPageProps {\n initialConfig?: Partial<VocaloidBoothConfig>;\n onSave?: (config: VocaloidBoothConfig) => Promise<void> | void;\n}\n\nexport const BoothConfigPage: React.FC<BoothConfigPageProps> = ({ initialConfig, onSave }) => {\n const [config, setConfig] = useState<VocaloidBoothConfig>(\n normalizeVocaloidBoothConfig(initialConfig)\n );\n const [saving, setSaving] = useState(false);\n\n const extText = useMemo(() => config.allowedExtensions.join(','), [config.allowedExtensions]);\n\n const update = <K extends keyof VocaloidBoothConfig>(key: K, value: VocaloidBoothConfig[K]) =>\n setConfig((prev) => ({ ...prev, [key]: value }));\n\n const save = async () => {\n setSaving(true);\n try {\n const normalized = normalizeVocaloidBoothConfig(config);\n setConfig(normalized);\n await onSave?.(normalized);\n } finally {\n setSaving(false);\n }\n };\n\n const reset = () => setConfig(defaultVocaloidBoothConfig);\n\n return (\n <div className=\"rounded-xl border border-slate-200 bg-white p-4 shadow-sm space-y-3\">\n <h3 className=\"text-lg font-semibold\">Vocaloid Booth 配置页</h3>\n\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-3 text-sm\">\n <input className=\"rounded border px-3 py-2\" value={config.boothId} onChange={(e) => update('boothId', e.target.value)} placeholder=\"boothId\" />\n <input className=\"rounded border px-3 py-2\" value={config.title} onChange={(e) => update('title', e.target.value)} placeholder=\"标题\" />\n <input className=\"rounded border px-3 py-2 md:col-span-2\" value={config.description ?? ''} onChange={(e) => update('description', e.target.value)} placeholder=\"描述\" />\n\n <input className=\"rounded border px-3 py-2\" type=\"number\" value={config.defaultTtlHours} onChange={(e) => update('defaultTtlHours', Number(e.target.value) || 1)} placeholder=\"默认保存时长(小时)\" />\n <input className=\"rounded border px-3 py-2\" type=\"number\" value={config.maxFiles} onChange={(e) => update('maxFiles', Number(e.target.value) || 1)} placeholder=\"最大文件数\" />\n <input className=\"rounded border px-3 py-2\" type=\"number\" value={config.maxSingleFileSizeMb} onChange={(e) => update('maxSingleFileSizeMb', Number(e.target.value) || 1)} placeholder=\"单文件上限 MB\" />\n <input className=\"rounded border px-3 py-2\" type=\"number\" value={config.maxTotalFileSizeMb} onChange={(e) => update('maxTotalFileSizeMb', Number(e.target.value) || 1)} placeholder=\"总大小上限 MB\" />\n\n <textarea\n className=\"rounded border px-3 py-2 md:col-span-2\"\n rows={3}\n value={extText}\n onChange={(e) =>\n update(\n 'allowedExtensions',\n e.target.value\n .split(',')\n .map((v) => v.trim())\n .filter(Boolean)\n )\n }\n placeholder=\"允许后缀,逗号分隔\"\n />\n </div>\n\n <div className=\"flex gap-2\">\n <button className=\"rounded bg-indigo-600 px-3 py-2 text-white\" disabled={saving} onClick={save}>\n {saving ? '保存中...' : '保存配置'}\n </button>\n <button className=\"rounded border px-3 py-2\" onClick={reset}>\n 恢复默认\n </button>\n </div>\n </div>\n );\n};\n"]}