sa2kit 1.6.88 → 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,83 @@
1
+ type BoothFileKind = 'project' | 'output' | 'asset' | 'other';
2
+ interface BoothFileItem {
3
+ id: string;
4
+ fileName: string;
5
+ size: number;
6
+ mimeType?: string;
7
+ objectKey: string;
8
+ checksum?: string;
9
+ kind?: BoothFileKind;
10
+ }
11
+ interface BoothUploadRecord {
12
+ id: string;
13
+ matchCode: string;
14
+ boothId: string;
15
+ createdAt: string;
16
+ expiresAt: string;
17
+ files: BoothFileItem[];
18
+ metadata?: {
19
+ nickname?: string;
20
+ contactTail?: string;
21
+ note?: string;
22
+ };
23
+ status: 'active' | 'expired' | 'deleted';
24
+ downloadCount: number;
25
+ }
26
+ type BoothAuditEventType = 'upload.created' | 'redeem.success' | 'redeem.failed' | 'redeem.blocked' | 'record.expired';
27
+ interface BoothAuditEvent {
28
+ type: BoothAuditEventType;
29
+ at: string;
30
+ boothId?: string;
31
+ recordId?: string;
32
+ matchCode?: string;
33
+ requesterKey?: string;
34
+ detail?: Record<string, unknown>;
35
+ }
36
+ interface CreateBoothUploadInput {
37
+ boothId: string;
38
+ ttlHours?: number;
39
+ metadata?: BoothUploadRecord['metadata'];
40
+ files: Omit<BoothFileItem, 'id'>[];
41
+ }
42
+ interface CreateBoothUploadResult {
43
+ record: BoothUploadRecord;
44
+ downloadUrlPath: string;
45
+ }
46
+ interface BoothVaultStore {
47
+ saveRecord(record: BoothUploadRecord): Promise<void>;
48
+ findByMatchCode(matchCode: string): Promise<BoothUploadRecord | null>;
49
+ findByRecordId?(recordId: string): Promise<BoothUploadRecord | null>;
50
+ incrementDownloadCount(recordId: string): Promise<void>;
51
+ existsByMatchCode(matchCode: string): Promise<boolean>;
52
+ }
53
+
54
+ interface BoothRedeemGuardLike {
55
+ assertAllowed(subjectKey: string): void;
56
+ registerAttempt(subjectKey: string, success: boolean): void;
57
+ }
58
+ interface BoothVaultServiceOptions {
59
+ store: BoothVaultStore;
60
+ codeLength?: number;
61
+ defaultTtlHours?: number;
62
+ baseDownloadPath?: string;
63
+ redeemGuard?: BoothRedeemGuardLike;
64
+ onAuditEvent?: (event: BoothAuditEvent) => void;
65
+ }
66
+ declare class BoothVaultService {
67
+ private emitAudit;
68
+ private readonly store;
69
+ private readonly codeLength;
70
+ private readonly defaultTtlHours;
71
+ private readonly baseDownloadPath;
72
+ private readonly redeemGuard?;
73
+ private readonly onAuditEvent?;
74
+ constructor(options: BoothVaultServiceOptions);
75
+ createUpload(input: CreateBoothUploadInput): Promise<CreateBoothUploadResult>;
76
+ getByMatchCode(matchCode: string): Promise<BoothUploadRecord | null>;
77
+ markDownloaded(recordId: string): Promise<void>;
78
+ resolveDownloadFilesByCode(matchCode: string, options?: {
79
+ requesterKey?: string;
80
+ }): Promise<BoothUploadRecord | null>;
81
+ }
82
+
83
+ export { type BoothVaultStore as B, type CreateBoothUploadInput as C, type BoothUploadRecord as a, type BoothFileItem as b, type BoothFileKind as c, BoothVaultService as d, type CreateBoothUploadResult as e, type BoothAuditEvent as f, type BoothAuditEventType as g, type BoothRedeemGuardLike as h, type BoothVaultServiceOptions as i };
@@ -0,0 +1,83 @@
1
+ type BoothFileKind = 'project' | 'output' | 'asset' | 'other';
2
+ interface BoothFileItem {
3
+ id: string;
4
+ fileName: string;
5
+ size: number;
6
+ mimeType?: string;
7
+ objectKey: string;
8
+ checksum?: string;
9
+ kind?: BoothFileKind;
10
+ }
11
+ interface BoothUploadRecord {
12
+ id: string;
13
+ matchCode: string;
14
+ boothId: string;
15
+ createdAt: string;
16
+ expiresAt: string;
17
+ files: BoothFileItem[];
18
+ metadata?: {
19
+ nickname?: string;
20
+ contactTail?: string;
21
+ note?: string;
22
+ };
23
+ status: 'active' | 'expired' | 'deleted';
24
+ downloadCount: number;
25
+ }
26
+ type BoothAuditEventType = 'upload.created' | 'redeem.success' | 'redeem.failed' | 'redeem.blocked' | 'record.expired';
27
+ interface BoothAuditEvent {
28
+ type: BoothAuditEventType;
29
+ at: string;
30
+ boothId?: string;
31
+ recordId?: string;
32
+ matchCode?: string;
33
+ requesterKey?: string;
34
+ detail?: Record<string, unknown>;
35
+ }
36
+ interface CreateBoothUploadInput {
37
+ boothId: string;
38
+ ttlHours?: number;
39
+ metadata?: BoothUploadRecord['metadata'];
40
+ files: Omit<BoothFileItem, 'id'>[];
41
+ }
42
+ interface CreateBoothUploadResult {
43
+ record: BoothUploadRecord;
44
+ downloadUrlPath: string;
45
+ }
46
+ interface BoothVaultStore {
47
+ saveRecord(record: BoothUploadRecord): Promise<void>;
48
+ findByMatchCode(matchCode: string): Promise<BoothUploadRecord | null>;
49
+ findByRecordId?(recordId: string): Promise<BoothUploadRecord | null>;
50
+ incrementDownloadCount(recordId: string): Promise<void>;
51
+ existsByMatchCode(matchCode: string): Promise<boolean>;
52
+ }
53
+
54
+ interface BoothRedeemGuardLike {
55
+ assertAllowed(subjectKey: string): void;
56
+ registerAttempt(subjectKey: string, success: boolean): void;
57
+ }
58
+ interface BoothVaultServiceOptions {
59
+ store: BoothVaultStore;
60
+ codeLength?: number;
61
+ defaultTtlHours?: number;
62
+ baseDownloadPath?: string;
63
+ redeemGuard?: BoothRedeemGuardLike;
64
+ onAuditEvent?: (event: BoothAuditEvent) => void;
65
+ }
66
+ declare class BoothVaultService {
67
+ private emitAudit;
68
+ private readonly store;
69
+ private readonly codeLength;
70
+ private readonly defaultTtlHours;
71
+ private readonly baseDownloadPath;
72
+ private readonly redeemGuard?;
73
+ private readonly onAuditEvent?;
74
+ constructor(options: BoothVaultServiceOptions);
75
+ createUpload(input: CreateBoothUploadInput): Promise<CreateBoothUploadResult>;
76
+ getByMatchCode(matchCode: string): Promise<BoothUploadRecord | null>;
77
+ markDownloaded(recordId: string): Promise<void>;
78
+ resolveDownloadFilesByCode(matchCode: string, options?: {
79
+ requesterKey?: string;
80
+ }): Promise<BoothUploadRecord | null>;
81
+ }
82
+
83
+ export { type BoothVaultStore as B, type CreateBoothUploadInput as C, type BoothUploadRecord as a, type BoothFileItem as b, type BoothFileKind as c, BoothVaultService as d, type CreateBoothUploadResult as e, type BoothAuditEvent as f, type BoothAuditEventType as g, type BoothRedeemGuardLike as h, type BoothVaultServiceOptions as i };
package/dist/index.d.mts CHANGED
@@ -18,6 +18,8 @@ export { u as useFestivalCardConfig } from './useFestivalCardConfig-DuiT7yuB.mjs
18
18
  export { FestivalCardBook3D, FestivalCardBook3DProps, FestivalCardConfigEditor, FestivalCardConfigPage, FestivalCardManagedPage, FestivalCardPageRenderer, FestivalCardStudio } from './festivalCard/web/index.mjs';
19
19
  export { F as FestivalCardService, c as createInMemoryFestivalCardDb } from './festivalCardService-Bsx2GfGI.mjs';
20
20
  export { j as FestivalCardAudioConfig, F as FestivalCardConfig, b as FestivalCardConfigSummary, c as FestivalCardDbAdapter, h as FestivalCardElement, e as FestivalCardElementBase, d as FestivalCardElementType, g as FestivalCardImageElement, i as FestivalCardPage, a as FestivalCardServiceOptions, f as FestivalCardTextElement } from './types-DBdb0jfs.mjs';
21
+ 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';
22
+ export { BoothConfigPage, BoothConfigPageProps, BoothRedeemPanel, BoothRedeemPanelProps, BoothSuccessCard, BoothSuccessCardProps, BoothUploadPanel, BoothUploadPanelProps, BoothUploadSubmitPayload, GenerateMatchCodeOptions, VocaloidBoothConfig, defaultVocaloidBoothConfig, generateMatchCode, normalizeMatchCode, normalizeVocaloidBoothConfig } from './vocaloidBooth/index.mjs';
21
23
  export { S as StorageAdapter, a as StorageChangeEvent } from './types-BaZccpvk.mjs';
22
24
  export { b as useAsyncStorage, d as useElectronStorage, a as useLocalStorage, u as useStorage, c as useTaroStorage } from './useElectronStorage-Dj0rcorG.mjs';
23
25
  import './types-CbTsi9CZ.mjs';
package/dist/index.d.ts CHANGED
@@ -18,6 +18,8 @@ export { u as useFestivalCardConfig } from './useFestivalCardConfig-BRbfBEv5.js'
18
18
  export { FestivalCardBook3D, FestivalCardBook3DProps, FestivalCardConfigEditor, FestivalCardConfigPage, FestivalCardManagedPage, FestivalCardPageRenderer, FestivalCardStudio } from './festivalCard/web/index.js';
19
19
  export { F as FestivalCardService, c as createInMemoryFestivalCardDb } from './festivalCardService-C58MuCpF.js';
20
20
  export { j as FestivalCardAudioConfig, F as FestivalCardConfig, b as FestivalCardConfigSummary, c as FestivalCardDbAdapter, h as FestivalCardElement, e as FestivalCardElementBase, d as FestivalCardElementType, g as FestivalCardImageElement, i as FestivalCardPage, a as FestivalCardServiceOptions, f as FestivalCardTextElement } from './types-DBdb0jfs.js';
21
+ 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';
22
+ export { BoothConfigPage, BoothConfigPageProps, BoothRedeemPanel, BoothRedeemPanelProps, BoothSuccessCard, BoothSuccessCardProps, BoothUploadPanel, BoothUploadPanelProps, BoothUploadSubmitPayload, GenerateMatchCodeOptions, VocaloidBoothConfig, defaultVocaloidBoothConfig, generateMatchCode, normalizeMatchCode, normalizeVocaloidBoothConfig } from './vocaloidBooth/index.js';
21
23
  export { S as StorageAdapter, a as StorageChangeEvent } from './types-BaZccpvk.js';
22
24
  export { b as useAsyncStorage, d as useElectronStorage, a as useLocalStorage, u as useStorage, c as useTaroStorage } from './useElectronStorage-DwnNfIhl.js';
23
25
  import './types-CbTsi9CZ.js';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var sortable = require('@dnd-kit/sortable');
14
14
  var utilities = require('@dnd-kit/utilities');
15
15
  var pgCore = require('drizzle-orm/pg-core');
16
16
  var drizzleOrm = require('drizzle-orm');
17
- require('crypto');
17
+ var crypto = require('crypto');
18
18
  require('bcryptjs');
19
19
  require('jsonwebtoken');
20
20
  var THREE2 = require('three');
@@ -8038,6 +8038,362 @@ var createInMemoryFestivalCardDb = () => ({
8038
8038
  }
8039
8039
  });
8040
8040
 
8041
+ // src/vocaloidBooth/core/code.ts
8042
+ var AMBIGUOUS = /* @__PURE__ */ new Set(["0", "1", "I", "O", "L"]);
8043
+ var ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789".split("").filter((c) => !AMBIGUOUS.has(c));
8044
+ var normalizeMatchCode = (value) => value.trim().toUpperCase();
8045
+ var generateMatchCode = async ({
8046
+ length = 6,
8047
+ maxAttempts = 20,
8048
+ exists
8049
+ }) => {
8050
+ if (length < 4) {
8051
+ throw new Error("Match code length must be at least 4");
8052
+ }
8053
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
8054
+ const code = Array.from({ length }).map(() => ALPHABET[Math.floor(Math.random() * ALPHABET.length)]).join("");
8055
+ if (!await exists(code)) {
8056
+ return code;
8057
+ }
8058
+ }
8059
+ throw new Error("Unable to generate unique match code");
8060
+ };
8061
+ var BoothVaultService = class {
8062
+ emitAudit(event) {
8063
+ this.onAuditEvent?.({
8064
+ ...event,
8065
+ at: (/* @__PURE__ */ new Date()).toISOString()
8066
+ });
8067
+ }
8068
+ constructor(options) {
8069
+ this.store = options.store;
8070
+ this.codeLength = options.codeLength ?? 6;
8071
+ this.defaultTtlHours = options.defaultTtlHours ?? 24 * 14;
8072
+ this.baseDownloadPath = options.baseDownloadPath ?? "/redeem";
8073
+ this.redeemGuard = options.redeemGuard;
8074
+ this.onAuditEvent = options.onAuditEvent;
8075
+ }
8076
+ async createUpload(input) {
8077
+ if (!input.files?.length) {
8078
+ throw new Error("At least one file is required");
8079
+ }
8080
+ const now = /* @__PURE__ */ new Date();
8081
+ const ttlHours = Math.max(1, input.ttlHours ?? this.defaultTtlHours);
8082
+ const expiresAt = new Date(now.getTime() + ttlHours * 60 * 60 * 1e3);
8083
+ const matchCode = await generateMatchCode({
8084
+ length: this.codeLength,
8085
+ exists: (code) => this.store.existsByMatchCode(code)
8086
+ });
8087
+ const record = {
8088
+ id: crypto.randomUUID(),
8089
+ boothId: input.boothId,
8090
+ matchCode,
8091
+ createdAt: now.toISOString(),
8092
+ expiresAt: expiresAt.toISOString(),
8093
+ files: input.files.map((file) => ({
8094
+ ...file,
8095
+ id: crypto.randomUUID()
8096
+ })),
8097
+ metadata: input.metadata,
8098
+ status: "active",
8099
+ downloadCount: 0
8100
+ };
8101
+ await this.store.saveRecord(record);
8102
+ this.emitAudit({
8103
+ type: "upload.created",
8104
+ boothId: record.boothId,
8105
+ recordId: record.id,
8106
+ matchCode: record.matchCode,
8107
+ detail: { fileCount: record.files.length }
8108
+ });
8109
+ return {
8110
+ record,
8111
+ downloadUrlPath: `${this.baseDownloadPath}?code=${record.matchCode}`
8112
+ };
8113
+ }
8114
+ async getByMatchCode(matchCode) {
8115
+ const normalized = normalizeMatchCode(matchCode);
8116
+ const record = await this.store.findByMatchCode(normalized);
8117
+ if (!record) {
8118
+ return null;
8119
+ }
8120
+ if (new Date(record.expiresAt).getTime() <= Date.now() && record.status === "active") {
8121
+ return {
8122
+ ...record,
8123
+ status: "expired"
8124
+ };
8125
+ }
8126
+ return record;
8127
+ }
8128
+ async markDownloaded(recordId) {
8129
+ await this.store.incrementDownloadCount(recordId);
8130
+ }
8131
+ async resolveDownloadFilesByCode(matchCode, options) {
8132
+ const requesterKey = options?.requesterKey;
8133
+ if (requesterKey && this.redeemGuard) {
8134
+ try {
8135
+ this.redeemGuard.assertAllowed(requesterKey);
8136
+ } catch (error) {
8137
+ this.emitAudit({
8138
+ type: "redeem.blocked",
8139
+ requesterKey,
8140
+ matchCode,
8141
+ detail: { message: error instanceof Error ? error.message : "blocked" }
8142
+ });
8143
+ throw error;
8144
+ }
8145
+ }
8146
+ const record = await this.getByMatchCode(matchCode);
8147
+ const success = !!record && record.status === "active";
8148
+ if (requesterKey && this.redeemGuard) {
8149
+ this.redeemGuard.registerAttempt(requesterKey, success);
8150
+ }
8151
+ if (!success) {
8152
+ this.emitAudit({
8153
+ type: "redeem.failed",
8154
+ requesterKey,
8155
+ matchCode,
8156
+ boothId: record?.boothId,
8157
+ recordId: record?.id
8158
+ });
8159
+ return record;
8160
+ }
8161
+ await this.markDownloaded(record.id);
8162
+ const reloaded = this.store.findByRecordId ? await this.store.findByRecordId(record.id) : await this.getByMatchCode(record.matchCode);
8163
+ this.emitAudit({
8164
+ type: "redeem.success",
8165
+ requesterKey,
8166
+ matchCode: record.matchCode,
8167
+ boothId: record.boothId,
8168
+ recordId: record.id
8169
+ });
8170
+ return reloaded ?? record;
8171
+ }
8172
+ };
8173
+
8174
+ // src/vocaloidBooth/core/config.ts
8175
+ var defaultVocaloidBoothConfig = {
8176
+ boothId: "default-booth",
8177
+ title: "MMD / Vocaloid \u521B\u4F5C\u6587\u4EF6\u5BC4\u5B58\u7AD9",
8178
+ description: "\u4E0A\u4F20\u521B\u4F5C\u6587\u4EF6\u5E76\u751F\u6210\u5339\u914D\u7801\uFF0C\u540E\u7EED\u53EF\u51ED\u7801\u4E0B\u8F7D",
8179
+ defaultTtlHours: 24 * 14,
8180
+ maxFiles: 20,
8181
+ maxSingleFileSizeMb: 2048,
8182
+ maxTotalFileSizeMb: 5120,
8183
+ allowedExtensions: ["zip", "7z", "rar", "vsqx", "vpr", "vmd", "pmx", "wav", "mp3", "mp4"]
8184
+ };
8185
+ var normalizeVocaloidBoothConfig = (input) => {
8186
+ const merged = {
8187
+ ...defaultVocaloidBoothConfig,
8188
+ ...input ?? {}
8189
+ };
8190
+ return {
8191
+ ...merged,
8192
+ boothId: merged.boothId || defaultVocaloidBoothConfig.boothId,
8193
+ title: merged.title || defaultVocaloidBoothConfig.title,
8194
+ defaultTtlHours: Math.max(1, merged.defaultTtlHours),
8195
+ maxFiles: Math.max(1, merged.maxFiles),
8196
+ maxSingleFileSizeMb: Math.max(1, merged.maxSingleFileSizeMb),
8197
+ maxTotalFileSizeMb: Math.max(1, merged.maxTotalFileSizeMb),
8198
+ allowedExtensions: (merged.allowedExtensions?.length ? merged.allowedExtensions : defaultVocaloidBoothConfig.allowedExtensions).map((ext) => ext.toLowerCase())
8199
+ };
8200
+ };
8201
+ var BoothUploadPanel = ({
8202
+ boothId,
8203
+ maxFiles = 10,
8204
+ maxFileSizeMb = 2048,
8205
+ accept,
8206
+ uploading = false,
8207
+ onSubmit
8208
+ }) => {
8209
+ const [files, setFiles] = React69.useState([]);
8210
+ const [nickname, setNickname] = React69.useState("");
8211
+ const [contactTail, setContactTail] = React69.useState("");
8212
+ const [ttlHours, setTtlHours] = React69.useState(24 * 14);
8213
+ const [error, setError] = React69.useState(null);
8214
+ const totalSizeMb = React69.useMemo(
8215
+ () => files.reduce((acc, file) => acc + file.size, 0) / 1024 / 1024,
8216
+ [files]
8217
+ );
8218
+ const addFiles = (newFiles) => {
8219
+ if (!newFiles) return;
8220
+ const incoming = Array.from(newFiles);
8221
+ const next = [...files, ...incoming];
8222
+ if (next.length > maxFiles) {
8223
+ setError(`\u6700\u591A\u4E0A\u4F20 ${maxFiles} \u4E2A\u6587\u4EF6`);
8224
+ return;
8225
+ }
8226
+ const oversized = incoming.find((f) => f.size > maxFileSizeMb * 1024 * 1024);
8227
+ if (oversized) {
8228
+ setError(`\u6587\u4EF6 ${oversized.name} \u8D85\u8FC7 ${maxFileSizeMb}MB \u9650\u5236`);
8229
+ return;
8230
+ }
8231
+ setError(null);
8232
+ setFiles(next);
8233
+ };
8234
+ const removeFile = (name) => setFiles((prev) => prev.filter((f) => f.name !== name));
8235
+ const handleSubmit = async () => {
8236
+ if (files.length === 0) {
8237
+ setError("\u8BF7\u5148\u9009\u62E9\u81F3\u5C11\u4E00\u4E2A\u6587\u4EF6");
8238
+ return;
8239
+ }
8240
+ setError(null);
8241
+ await onSubmit({
8242
+ boothId,
8243
+ files,
8244
+ nickname: nickname || void 0,
8245
+ contactTail: contactTail || void 0,
8246
+ ttlHours
8247
+ });
8248
+ };
8249
+ return /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "rounded-xl border border-slate-200 bg-white p-4 shadow-sm" }, /* @__PURE__ */ React69__namespace.default.createElement("h3", { className: "mb-3 text-lg font-semibold" }, "\u4E0A\u4F20\u521B\u4F5C\u6587\u4EF6"), /* @__PURE__ */ React69__namespace.default.createElement(
8250
+ "input",
8251
+ {
8252
+ type: "file",
8253
+ multiple: true,
8254
+ accept,
8255
+ onChange: (e) => addFiles(e.target.files),
8256
+ className: "mb-3 block w-full text-sm"
8257
+ }
8258
+ ), /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "mb-3 grid grid-cols-1 gap-2 md:grid-cols-3" }, /* @__PURE__ */ React69__namespace.default.createElement(
8259
+ "input",
8260
+ {
8261
+ value: nickname,
8262
+ onChange: (e) => setNickname(e.target.value),
8263
+ placeholder: "\u6635\u79F0\uFF08\u53EF\u9009\uFF09",
8264
+ className: "rounded-md border px-3 py-2 text-sm"
8265
+ }
8266
+ ), /* @__PURE__ */ React69__namespace.default.createElement(
8267
+ "input",
8268
+ {
8269
+ value: contactTail,
8270
+ onChange: (e) => setContactTail(e.target.value),
8271
+ placeholder: "\u8054\u7CFB\u65B9\u5F0F\u540E4\u4F4D\uFF08\u53EF\u9009\uFF09",
8272
+ className: "rounded-md border px-3 py-2 text-sm"
8273
+ }
8274
+ ), /* @__PURE__ */ React69__namespace.default.createElement(
8275
+ "input",
8276
+ {
8277
+ value: ttlHours,
8278
+ type: "number",
8279
+ min: 1,
8280
+ onChange: (e) => setTtlHours(Number(e.target.value) || 24),
8281
+ placeholder: "\u4FDD\u5B58\u65F6\u957F\uFF08\u5C0F\u65F6\uFF09",
8282
+ className: "rounded-md border px-3 py-2 text-sm"
8283
+ }
8284
+ )), /* @__PURE__ */ React69__namespace.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__ */ React69__namespace.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__ */ React69__namespace.default.createElement("li", { className: "text-slate-400" }, "\u5C1A\u672A\u9009\u62E9\u6587\u4EF6"), files.map((file) => /* @__PURE__ */ React69__namespace.default.createElement("li", { key: `${file.name}-${file.size}`, className: "mb-1 flex items-center justify-between gap-2" }, /* @__PURE__ */ React69__namespace.default.createElement("span", { className: "truncate" }, file.name), /* @__PURE__ */ React69__namespace.default.createElement("button", { type: "button", className: "text-rose-500", onClick: () => removeFile(file.name) }, "\u79FB\u9664")))), error && /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "mb-3 rounded-md bg-rose-50 p-2 text-sm text-rose-700" }, error), /* @__PURE__ */ React69__namespace.default.createElement(
8285
+ "button",
8286
+ {
8287
+ type: "button",
8288
+ disabled: uploading,
8289
+ onClick: handleSubmit,
8290
+ className: "rounded-md bg-indigo-600 px-3 py-2 text-white disabled:cursor-not-allowed disabled:opacity-50"
8291
+ },
8292
+ uploading ? "\u4E0A\u4F20\u4E2D..." : "\u5F00\u59CB\u4E0A\u4F20"
8293
+ ));
8294
+ };
8295
+ var BoothRedeemPanel = ({ onRedeem, loading }) => {
8296
+ const [matchCode, setMatchCode] = React69.useState("");
8297
+ const [record, setRecord] = React69.useState(null);
8298
+ const [error, setError] = React69.useState(null);
8299
+ const handleRedeem = async () => {
8300
+ setError(null);
8301
+ const result = await onRedeem(matchCode.trim());
8302
+ if (!result) {
8303
+ setRecord(null);
8304
+ setError("\u5339\u914D\u7801\u4E0D\u5B58\u5728\uFF0C\u8BF7\u68C0\u67E5\u540E\u91CD\u8BD5");
8305
+ return;
8306
+ }
8307
+ if (result.status !== "active") {
8308
+ setRecord(result);
8309
+ setError("\u5339\u914D\u7801\u5DF2\u8FC7\u671F\u6216\u5931\u6548");
8310
+ return;
8311
+ }
8312
+ setRecord(result);
8313
+ };
8314
+ return /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "rounded-xl border border-slate-200 bg-white p-4 shadow-sm" }, /* @__PURE__ */ React69__namespace.default.createElement("h3", { className: "mb-3 text-lg font-semibold" }, "\u51ED\u5339\u914D\u7801\u4E0B\u8F7D"), /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "mb-3 flex gap-2" }, /* @__PURE__ */ React69__namespace.default.createElement(
8315
+ "input",
8316
+ {
8317
+ value: matchCode,
8318
+ onChange: (e) => setMatchCode(e.target.value.toUpperCase()),
8319
+ placeholder: "\u8F93\u5165\u5339\u914D\u7801\uFF08\u5982 A7K9Q2\uFF09",
8320
+ className: "w-full rounded-md border px-3 py-2 text-sm uppercase"
8321
+ }
8322
+ ), /* @__PURE__ */ React69__namespace.default.createElement(
8323
+ "button",
8324
+ {
8325
+ type: "button",
8326
+ onClick: handleRedeem,
8327
+ disabled: loading,
8328
+ className: "rounded-md bg-slate-900 px-3 py-2 text-white disabled:opacity-50"
8329
+ },
8330
+ "\u67E5\u8BE2"
8331
+ )), error && /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "mb-3 rounded-md bg-amber-50 p-2 text-sm text-amber-700" }, error), record && /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "rounded-md border border-slate-100 p-3" }, /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "mb-2 text-xs text-slate-500" }, "\u5171 ", record.files.length, " \u4E2A\u6587\u4EF6"), /* @__PURE__ */ React69__namespace.default.createElement("ul", { className: "space-y-1 text-sm" }, record.files.map((file) => /* @__PURE__ */ React69__namespace.default.createElement("li", { key: file.id, className: "flex items-center justify-between gap-2" }, /* @__PURE__ */ React69__namespace.default.createElement("span", { className: "truncate" }, file.fileName), /* @__PURE__ */ React69__namespace.default.createElement("a", { href: file.objectKey, className: "text-indigo-600 hover:underline", download: true }, "\u4E0B\u8F7D"))))));
8332
+ };
8333
+ var BoothSuccessCard = ({
8334
+ matchCode,
8335
+ expiresAt,
8336
+ downloadUrlPath,
8337
+ onCopyCode,
8338
+ className
8339
+ }) => {
8340
+ const handleCopy = async () => {
8341
+ try {
8342
+ await navigator.clipboard.writeText(matchCode);
8343
+ } catch {
8344
+ }
8345
+ onCopyCode?.(matchCode);
8346
+ };
8347
+ return /* @__PURE__ */ React69__namespace.default.createElement("div", { className: `rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm ${className ?? ""}` }, /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "mb-2 text-xs text-emerald-800" }, "\u4E0A\u4F20\u5B8C\u6210\uFF0C\u5DF2\u751F\u6210\u5339\u914D\u7801"), /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "mb-3 text-2xl font-bold tracking-widest text-emerald-900" }, matchCode), /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "mb-3 text-xs text-emerald-800" }, "\u8FC7\u671F\u65F6\u95F4\uFF1A", new Date(expiresAt).toLocaleString()), /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ React69__namespace.default.createElement(
8348
+ "button",
8349
+ {
8350
+ type: "button",
8351
+ onClick: handleCopy,
8352
+ className: "rounded-md bg-emerald-600 px-3 py-2 text-white hover:bg-emerald-700"
8353
+ },
8354
+ "\u590D\u5236\u5339\u914D\u7801"
8355
+ ), /* @__PURE__ */ React69__namespace.default.createElement(
8356
+ "a",
8357
+ {
8358
+ href: downloadUrlPath,
8359
+ className: "rounded-md border border-emerald-400 bg-white px-3 py-2 text-emerald-700 hover:bg-emerald-100"
8360
+ },
8361
+ "\u6253\u5F00\u4E0B\u8F7D\u9875"
8362
+ )));
8363
+ };
8364
+ var BoothConfigPage = ({ initialConfig, onSave }) => {
8365
+ const [config, setConfig] = React69.useState(
8366
+ normalizeVocaloidBoothConfig(initialConfig)
8367
+ );
8368
+ const [saving, setSaving] = React69.useState(false);
8369
+ const extText = React69.useMemo(() => config.allowedExtensions.join(","), [config.allowedExtensions]);
8370
+ const update = (key, value) => setConfig((prev) => ({ ...prev, [key]: value }));
8371
+ const save = async () => {
8372
+ setSaving(true);
8373
+ try {
8374
+ const normalized = normalizeVocaloidBoothConfig(config);
8375
+ setConfig(normalized);
8376
+ await onSave?.(normalized);
8377
+ } finally {
8378
+ setSaving(false);
8379
+ }
8380
+ };
8381
+ const reset = () => setConfig(defaultVocaloidBoothConfig);
8382
+ return /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "rounded-xl border border-slate-200 bg-white p-4 shadow-sm space-y-3" }, /* @__PURE__ */ React69__namespace.default.createElement("h3", { className: "text-lg font-semibold" }, "Vocaloid Booth \u914D\u7F6E\u9875"), /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm" }, /* @__PURE__ */ React69__namespace.default.createElement("input", { className: "rounded border px-3 py-2", value: config.boothId, onChange: (e) => update("boothId", e.target.value), placeholder: "boothId" }), /* @__PURE__ */ React69__namespace.default.createElement("input", { className: "rounded border px-3 py-2", value: config.title, onChange: (e) => update("title", e.target.value), placeholder: "\u6807\u9898" }), /* @__PURE__ */ React69__namespace.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__ */ React69__namespace.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__ */ React69__namespace.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__ */ React69__namespace.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__ */ React69__namespace.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__ */ React69__namespace.default.createElement(
8383
+ "textarea",
8384
+ {
8385
+ className: "rounded border px-3 py-2 md:col-span-2",
8386
+ rows: 3,
8387
+ value: extText,
8388
+ onChange: (e) => update(
8389
+ "allowedExtensions",
8390
+ e.target.value.split(",").map((v) => v.trim()).filter(Boolean)
8391
+ ),
8392
+ placeholder: "\u5141\u8BB8\u540E\u7F00\uFF0C\u9017\u53F7\u5206\u9694"
8393
+ }
8394
+ )), /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ React69__namespace.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__ */ React69__namespace.default.createElement("button", { className: "rounded border px-3 py-2", onClick: reset }, "\u6062\u590D\u9ED8\u8BA4")));
8395
+ };
8396
+
8041
8397
  // src/storage/adapters/react-native-adapter.ts
8042
8398
  var AsyncStorage = null;
8043
8399
  try {
@@ -8410,6 +8766,11 @@ exports.AvatarImage = AvatarImage;
8410
8766
  exports.BackButton = BackButton;
8411
8767
  exports.BackgroundRemover = BackgroundRemover;
8412
8768
  exports.Badge = Badge;
8769
+ exports.BoothConfigPage = BoothConfigPage;
8770
+ exports.BoothRedeemPanel = BoothRedeemPanel;
8771
+ exports.BoothSuccessCard = BoothSuccessCard;
8772
+ exports.BoothUploadPanel = BoothUploadPanel;
8773
+ exports.BoothVaultService = BoothVaultService;
8413
8774
  exports.Button = Button;
8414
8775
  exports.Card = Card;
8415
8776
  exports.CardContent = CardContent;
@@ -8558,10 +8919,12 @@ exports.createLogger = createLogger;
8558
8919
  exports.createOpenAICompatibleProvider = createOpenAICompatibleProvider;
8559
8920
  exports.createSkillRegistry = createSkillRegistry;
8560
8921
  exports.debugUtils = debugUtils;
8922
+ exports.defaultVocaloidBoothConfig = defaultVocaloidBoothConfig;
8561
8923
  exports.errorUtils = errorUtils;
8562
8924
  exports.fileUtils = fileUtils;
8563
8925
  exports.filterExperiments = filterExperiments;
8564
8926
  exports.formatTime = formatTime;
8927
+ exports.generateMatchCode = generateMatchCode;
8565
8928
  exports.getAllTags = getAllTags;
8566
8929
  exports.getCategoryColor = getCategoryColor;
8567
8930
  exports.getCategoryDisplayName = getCategoryDisplayName;
@@ -8572,7 +8935,9 @@ exports.getExperimentCounts = getExperimentCounts;
8572
8935
  exports.japaneseUtils = japaneseUtils;
8573
8936
  exports.logger = logger;
8574
8937
  exports.normalizeFestivalCardConfig = normalizeFestivalCardConfig;
8938
+ exports.normalizeMatchCode = normalizeMatchCode;
8575
8939
  exports.normalizePromptVariables = normalizePromptVariables;
8940
+ exports.normalizeVocaloidBoothConfig = normalizeVocaloidBoothConfig;
8576
8941
  exports.resizeFestivalCardPages = resizeFestivalCardPages;
8577
8942
  exports.resolveScreenReceiverSignalUrl = resolveScreenReceiverSignalUrl;
8578
8943
  exports.skillToToolDefinition = skillToToolDefinition;