sa2kit 1.6.91 → 1.6.92

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 (65) hide show
  1. package/dist/client-BlkUL2To.d.ts +26 -0
  2. package/dist/client-DpMIhrlS.d.mts +26 -0
  3. package/dist/index-C7yh6b5Q.d.mts +17 -0
  4. package/dist/index-CDapUIT5.d.mts +51 -0
  5. package/dist/index-Cv9jlnNz.d.ts +17 -0
  6. package/dist/index-D3UbkUai.d.ts +51 -0
  7. package/dist/index.d.mts +11 -0
  8. package/dist/index.d.ts +11 -0
  9. package/dist/index.js +1337 -58
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +1306 -61
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/mikuContest/index.d.mts +13 -0
  14. package/dist/mikuContest/index.d.ts +13 -0
  15. package/dist/mikuContest/index.js +1310 -0
  16. package/dist/mikuContest/index.js.map +1 -0
  17. package/dist/mikuContest/index.mjs +1253 -0
  18. package/dist/mikuContest/index.mjs.map +1 -0
  19. package/dist/mikuContest/logic/index.d.mts +32 -0
  20. package/dist/mikuContest/logic/index.d.ts +32 -0
  21. package/dist/mikuContest/logic/index.js +511 -0
  22. package/dist/mikuContest/logic/index.js.map +1 -0
  23. package/dist/mikuContest/logic/index.mjs +483 -0
  24. package/dist/mikuContest/logic/index.mjs.map +1 -0
  25. package/dist/mikuContest/routes/index.d.mts +80 -0
  26. package/dist/mikuContest/routes/index.d.ts +80 -0
  27. package/dist/mikuContest/routes/index.js +821 -0
  28. package/dist/mikuContest/routes/index.js.map +1 -0
  29. package/dist/mikuContest/routes/index.mjs +791 -0
  30. package/dist/mikuContest/routes/index.mjs.map +1 -0
  31. package/dist/mikuContest/server/index.d.mts +766 -0
  32. package/dist/mikuContest/server/index.d.ts +766 -0
  33. package/dist/mikuContest/server/index.js +705 -0
  34. package/dist/mikuContest/server/index.js.map +1 -0
  35. package/dist/mikuContest/server/index.mjs +672 -0
  36. package/dist/mikuContest/server/index.mjs.map +1 -0
  37. package/dist/mikuContest/service/index.d.mts +30 -0
  38. package/dist/mikuContest/service/index.d.ts +30 -0
  39. package/dist/mikuContest/service/index.js +139 -0
  40. package/dist/mikuContest/service/index.js.map +1 -0
  41. package/dist/mikuContest/service/index.mjs +135 -0
  42. package/dist/mikuContest/service/index.mjs.map +1 -0
  43. package/dist/mikuContest/types/index.d.mts +179 -0
  44. package/dist/mikuContest/types/index.d.ts +179 -0
  45. package/dist/mikuContest/types/index.js +4 -0
  46. package/dist/mikuContest/types/index.js.map +1 -0
  47. package/dist/mikuContest/types/index.mjs +3 -0
  48. package/dist/mikuContest/types/index.mjs.map +1 -0
  49. package/dist/mikuContest/ui/miniapp/index.d.mts +3 -0
  50. package/dist/mikuContest/ui/miniapp/index.d.ts +3 -0
  51. package/dist/mikuContest/ui/miniapp/index.js +566 -0
  52. package/dist/mikuContest/ui/miniapp/index.js.map +1 -0
  53. package/dist/mikuContest/ui/miniapp/index.mjs +540 -0
  54. package/dist/mikuContest/ui/miniapp/index.mjs.map +1 -0
  55. package/dist/mikuContest/ui/web/index.d.mts +4 -0
  56. package/dist/mikuContest/ui/web/index.d.ts +4 -0
  57. package/dist/mikuContest/ui/web/index.js +353 -0
  58. package/dist/mikuContest/ui/web/index.js.map +1 -0
  59. package/dist/mikuContest/ui/web/index.mjs +343 -0
  60. package/dist/mikuContest/ui/web/index.mjs.map +1 -0
  61. package/dist/service-D7DM1wW-.d.ts +38 -0
  62. package/dist/service-DPr2rlvH.d.mts +38 -0
  63. package/dist/types-BS7Xz09b.d.mts +14 -0
  64. package/dist/types-k4koMp4m.d.ts +14 -0
  65. package/package.json +41 -1
package/dist/index.mjs CHANGED
@@ -11,19 +11,26 @@ import Link from 'next/link';
11
11
  import { useSensors, useSensor, PointerSensor, TouchSensor, KeyboardSensor, DndContext, closestCenter } from '@dnd-kit/core';
12
12
  import { useSortable, sortableKeyboardCoordinates, SortableContext, rectSortingStrategy, arrayMove } from '@dnd-kit/sortable';
13
13
  import { CSS } from '@dnd-kit/utilities';
14
- import { pgEnum, pgTable, boolean, timestamp, jsonb, text, uniqueIndex, foreignKey } from 'drizzle-orm/pg-core';
15
- import { sql, relations } from 'drizzle-orm';
14
+ import { pgEnum, pgTable, boolean, timestamp, jsonb, text, uniqueIndex, foreignKey, integer } from 'drizzle-orm/pg-core';
15
+ import { sql, relations, eq } from 'drizzle-orm';
16
16
  import { randomUUID } from 'crypto';
17
17
  import 'bcryptjs';
18
18
  import 'jsonwebtoken';
19
19
  import * as THREE2 from 'three';
20
+ import * as XLSX from 'xlsx';
21
+ import { NextResponse } from 'next/server';
20
22
 
23
+ var __defProp = Object.defineProperty;
21
24
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
22
25
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
23
26
  }) : x)(function(x) {
24
27
  if (typeof require !== "undefined") return require.apply(this, arguments);
25
28
  throw Error('Dynamic require of "' + x + '" is not supported');
26
29
  });
30
+ var __export = (target, all) => {
31
+ for (var name in all)
32
+ __defProp(target, name, { get: all[name], enumerable: true });
33
+ };
27
34
 
28
35
  // src/logger/types.ts
29
36
  var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
@@ -51,10 +58,10 @@ var ConsoleLoggerAdapter = class {
51
58
  };
52
59
  }
53
60
  log(entry) {
54
- const { level, message, timestamp: timestamp5, data, context, error } = entry;
61
+ const { level, message, timestamp: timestamp6, data, context, error } = entry;
55
62
  let logMessage = "";
56
- if (timestamp5) {
57
- logMessage += "[" + this.formatTimestamp(timestamp5) + "] ";
63
+ if (timestamp6) {
64
+ logMessage += "[" + this.formatTimestamp(timestamp6) + "] ";
58
65
  }
59
66
  const levelName = this.getLevelName(level);
60
67
  logMessage += levelName + ": ";
@@ -547,7 +554,7 @@ var useSentimentAnalysis = (options = {}) => {
547
554
  result: null
548
555
  });
549
556
  const pipelineRef = useRef(null);
550
- const analyze = useCallback(async (text5) => {
557
+ const analyze = useCallback(async (text6) => {
551
558
  setState((prev) => ({
552
559
  ...prev,
553
560
  isProcessing: true,
@@ -594,12 +601,12 @@ var useSentimentAnalysis = (options = {}) => {
594
601
  pipelineRef.current = await pipeline("sentiment-analysis", options.model || defaultModel);
595
602
  }
596
603
  setState((prev) => ({ ...prev, status: "analyzing" }));
597
- const output = await pipelineRef.current(text5);
604
+ const output = await pipelineRef.current(text6);
598
605
  const resultData = output[0];
599
606
  const label = resultData.label.toLowerCase();
600
607
  let sentiment = "neutral";
601
608
  const negativeKeywords = ["\u7D2F", "\u60E8", "\u7EDD\u671B", "\u96BE\u53D7", "\u4F24\u5FC3", "\u5DEE", "\u574F", "\u7CDF", "\u4E0D\u884C"];
602
- const hasNegativeKeyword = negativeKeywords.some((k) => text5.includes(k));
609
+ const hasNegativeKeyword = negativeKeywords.some((k) => text6.includes(k));
603
610
  if (label.includes("positive") && !hasNegativeKeyword) {
604
611
  sentiment = "positive";
605
612
  } else if (label.includes("negative") || label.includes("0") || hasNegativeKeyword) {
@@ -631,12 +638,12 @@ var SentimentAnalyzer = ({
631
638
  className = "",
632
639
  placeholder = "\u8F93\u5165\u4E00\u6BB5\u4E2D\u6587\u6216\u82F1\u6587\uFF0C\u5206\u6790\u5176\u60C5\u611F\u503E\u5411..."
633
640
  }) => {
634
- const [text5, setText] = useState("");
641
+ const [text6, setText] = useState("");
635
642
  const { analyze, isProcessing, status, result, error } = useSentimentAnalysis();
636
643
  const handleAnalyze = async () => {
637
- if (!text5.trim() || isProcessing) return;
644
+ if (!text6.trim() || isProcessing) return;
638
645
  try {
639
- const res = await analyze(text5);
646
+ const res = await analyze(text6);
640
647
  onResult?.(res);
641
648
  } catch (err) {
642
649
  console.error("Sentiment Analysis Error:", err);
@@ -667,7 +674,7 @@ var SentimentAnalyzer = ({
667
674
  return /* @__PURE__ */ React69__default.createElement("div", { className: clsx("p-6 border rounded-xl bg-white dark:bg-gray-800 shadow-sm", className) }, /* @__PURE__ */ React69__default.createElement("div", { className: "flex items-center gap-2 mb-4 text-gray-700 dark:text-gray-300 font-medium" }, /* @__PURE__ */ React69__default.createElement(MessageSquare, { size: 20 }), /* @__PURE__ */ React69__default.createElement("span", null, "\u6587\u672C\u60C5\u611F\u5206\u6790")), /* @__PURE__ */ React69__default.createElement("div", { className: "relative" }, /* @__PURE__ */ React69__default.createElement(
668
675
  "textarea",
669
676
  {
670
- value: text5,
677
+ value: text6,
671
678
  onChange: (e) => setText(e.target.value),
672
679
  placeholder,
673
680
  className: "w-full h-32 p-4 bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all outline-none resize-none text-gray-800 dark:text-gray-200",
@@ -677,7 +684,7 @@ var SentimentAnalyzer = ({
677
684
  "button",
678
685
  {
679
686
  onClick: handleAnalyze,
680
- disabled: !text5.trim() || isProcessing,
687
+ disabled: !text6.trim() || isProcessing,
681
688
  className: "absolute bottom-3 right-3 p-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-md transition-colors shadow-sm"
682
689
  },
683
690
  isProcessing ? /* @__PURE__ */ React69__default.createElement(Loader2, { className: "animate-spin", size: 18 }) : /* @__PURE__ */ React69__default.createElement(Send, { size: 18 })
@@ -865,13 +872,13 @@ var requestJson = async (options) => {
865
872
  body: body ? JSON.stringify(body) : void 0,
866
873
  signal: controller?.signal
867
874
  });
868
- const text5 = await response.text();
875
+ const text6 = await response.text();
869
876
  let data = null;
870
- if (text5) {
877
+ if (text6) {
871
878
  try {
872
- data = JSON.parse(text5);
879
+ data = JSON.parse(text6);
873
880
  } catch {
874
- data = text5;
881
+ data = text6;
875
882
  }
876
883
  }
877
884
  if (!response.ok) {
@@ -1332,20 +1339,20 @@ var japaneseUtils = {
1332
1339
  /**
1333
1340
  * 提取文本中的汉字
1334
1341
  */
1335
- extractKanji(text5) {
1336
- return text5.match(/[\u4E00-\u9FAF]/g) || [];
1342
+ extractKanji(text6) {
1343
+ return text6.match(/[\u4E00-\u9FAF]/g) || [];
1337
1344
  },
1338
1345
  /**
1339
1346
  * 提取文本中的假名
1340
1347
  */
1341
- extractKana(text5) {
1342
- return text5.match(/[\u3040-\u309F\u30A0-\u30FF]/g) || [];
1348
+ extractKana(text6) {
1349
+ return text6.match(/[\u3040-\u309F\u30A0-\u30FF]/g) || [];
1343
1350
  },
1344
1351
  /**
1345
1352
  * 清理文本,移除特殊字符但保留日语字符
1346
1353
  */
1347
- cleanText(text5) {
1348
- return text5.replace(/[^\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\w\s]/g, "");
1354
+ cleanText(text6) {
1355
+ return text6.replace(/[^\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\w\s]/g, "");
1349
1356
  }
1350
1357
  };
1351
1358
 
@@ -1434,11 +1441,11 @@ var fileUtils = {
1434
1441
  * 生成唯一文件名
1435
1442
  */
1436
1443
  generateUniqueFileName(originalName) {
1437
- const timestamp5 = Date.now();
1444
+ const timestamp6 = Date.now();
1438
1445
  const random = Math.random().toString(36).substring(2, 15);
1439
1446
  const extension = this.getFileExtension(originalName);
1440
1447
  const baseName = originalName.replace("." + extension, "");
1441
- return extension ? baseName + "_" + timestamp5 + "_" + random + "." + extension : baseName + "_" + timestamp5 + "_" + random;
1448
+ return extension ? baseName + "_" + timestamp6 + "_" + random + "." + extension : baseName + "_" + timestamp6 + "_" + random;
1442
1449
  },
1443
1450
  /**
1444
1451
  * 验证文件名是否有效
@@ -1508,28 +1515,28 @@ var stringUtils = {
1508
1515
  /**
1509
1516
  * 截断文本
1510
1517
  */
1511
- truncate(text5, length, suffix = "...") {
1512
- if (text5.length <= length) return text5;
1513
- return text5.substring(0, length - suffix.length) + suffix;
1518
+ truncate(text6, length, suffix = "...") {
1519
+ if (text6.length <= length) return text6;
1520
+ return text6.substring(0, length - suffix.length) + suffix;
1514
1521
  },
1515
1522
  /**
1516
1523
  * 首字母大写
1517
1524
  */
1518
- capitalize(text5) {
1519
- if (!text5) return "";
1520
- return text5.charAt(0).toUpperCase() + text5.slice(1).toLowerCase();
1525
+ capitalize(text6) {
1526
+ if (!text6) return "";
1527
+ return text6.charAt(0).toUpperCase() + text6.slice(1).toLowerCase();
1521
1528
  },
1522
1529
  /**
1523
1530
  * 驼峰转下划线
1524
1531
  */
1525
- camelToSnake(text5) {
1526
- return text5.replace(/[A-Z]/g, (letter) => "_" + letter.toLowerCase());
1532
+ camelToSnake(text6) {
1533
+ return text6.replace(/[A-Z]/g, (letter) => "_" + letter.toLowerCase());
1527
1534
  },
1528
1535
  /**
1529
1536
  * 下划线转驼峰
1530
1537
  */
1531
- snakeToCamel(text5) {
1532
- return text5.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
1538
+ snakeToCamel(text6) {
1539
+ return text6.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
1533
1540
  },
1534
1541
  /**
1535
1542
  * 生成随机字符串
@@ -5648,9 +5655,9 @@ var UserInfoBar = ({ apiClient }) => {
5648
5655
  ))));
5649
5656
  };
5650
5657
  function DanmakuPanel({ onSend }) {
5651
- const [text5, setText] = useState("");
5658
+ const [text6, setText] = useState("");
5652
5659
  const emit = () => {
5653
- const value = text5.trim();
5660
+ const value = text6.trim();
5654
5661
  if (!value) {
5655
5662
  return;
5656
5663
  }
@@ -5661,7 +5668,7 @@ function DanmakuPanel({ onSend }) {
5661
5668
  "input",
5662
5669
  {
5663
5670
  type: "text",
5664
- value: text5,
5671
+ value: text6,
5665
5672
  onChange: (event) => setText(event.target.value),
5666
5673
  onKeyDown: (event) => {
5667
5674
  if (event.key === "Enter") {
@@ -5777,8 +5784,8 @@ function useDanmakuController(options) {
5777
5784
  cursorRef.current += 1;
5778
5785
  }, []);
5779
5786
  const send = useCallback(
5780
- (text5, color, sendOptions) => {
5781
- const trimmed = text5.trim();
5787
+ (text6, color, sendOptions) => {
5788
+ const trimmed = text6.trim();
5782
5789
  if (!trimmed) {
5783
5790
  return null;
5784
5791
  }
@@ -5815,26 +5822,26 @@ function useDanmakuController(options) {
5815
5822
  [addIncoming, items, removeItem, send]
5816
5823
  );
5817
5824
  }
5818
- function parseCommand(text5) {
5819
- if (text5.startsWith("/miku ")) {
5820
- return { launchKind: "miku", content: text5.replace("/miku ", "").trim() };
5825
+ function parseCommand(text6) {
5826
+ if (text6.startsWith("/miku ")) {
5827
+ return { launchKind: "miku", content: text6.replace("/miku ", "").trim() };
5821
5828
  }
5822
- if (text5 === "/miku") {
5829
+ if (text6 === "/miku") {
5823
5830
  return { launchKind: "miku", content: "MIKU!" };
5824
5831
  }
5825
- if (text5.startsWith("/avatar ")) {
5826
- return { launchKind: "avatar", content: text5.replace("/avatar ", "").trim() };
5832
+ if (text6.startsWith("/avatar ")) {
5833
+ return { launchKind: "avatar", content: text6.replace("/avatar ", "").trim() };
5827
5834
  }
5828
- if (text5 === "/avatar") {
5835
+ if (text6 === "/avatar") {
5829
5836
  return { launchKind: "avatar", content: "Avatar Firework!" };
5830
5837
  }
5831
- if (text5.startsWith("/normal ")) {
5832
- return { launchKind: "normal", content: text5.replace("/normal ", "").trim() };
5838
+ if (text6.startsWith("/normal ")) {
5839
+ return { launchKind: "normal", content: text6.replace("/normal ", "").trim() };
5833
5840
  }
5834
- if (text5 === "/normal") {
5841
+ if (text6 === "/normal") {
5835
5842
  return { launchKind: "normal", content: "Fireworks!" };
5836
5843
  }
5837
- return { content: text5 };
5844
+ return { content: text6 };
5838
5845
  }
5839
5846
  function createCircularSpriteTexture() {
5840
5847
  const size = 64;
@@ -6516,12 +6523,12 @@ var WebSocketTransport = class {
6516
6523
  }
6517
6524
  };
6518
6525
  function parseServerMessage(raw) {
6519
- const text5 = decodeMessage(raw);
6520
- if (!text5) {
6526
+ const text6 = decodeMessage(raw);
6527
+ if (!text6) {
6521
6528
  return null;
6522
6529
  }
6523
6530
  try {
6524
- return JSON.parse(text5);
6531
+ return JSON.parse(text6);
6525
6532
  } catch {
6526
6533
  return null;
6527
6534
  }
@@ -6788,8 +6795,8 @@ function MikuFireworks3D({
6788
6795
  }
6789
6796
  launch(payload);
6790
6797
  };
6791
- const handleSendDanmaku = (text5) => {
6792
- const result = send(text5, void 0, {
6798
+ const handleSendDanmaku = (text6) => {
6799
+ const result = send(text6, void 0, {
6793
6800
  optimistic: !realtimeEnabled
6794
6801
  });
6795
6802
  if (!result) {
@@ -6888,9 +6895,9 @@ function useScreenReceiver(options) {
6888
6895
  const peerRef = useRef({ pendingCandidates: [] });
6889
6896
  const videoRef = useRef(null);
6890
6897
  const appendLog = useCallback(
6891
- (text5) => {
6898
+ (text6) => {
6892
6899
  logIdRef.current += 1;
6893
- setLogs((prev) => [...prev, { id: logIdRef.current, text: text5 }].slice(-maxLogs));
6900
+ setLogs((prev) => [...prev, { id: logIdRef.current, text: text6 }].slice(-maxLogs));
6894
6901
  },
6895
6902
  [maxLogs]
6896
6903
  );
@@ -7616,8 +7623,8 @@ var withRoundedClip = (ctx, left, top, width, height, radius, draw) => {
7616
7623
  draw();
7617
7624
  ctx.restore();
7618
7625
  };
7619
- var drawMultilineText = (ctx, text5, left, top, maxWidth, lineHeight) => {
7620
- const paragraphs = text5.split("\n");
7626
+ var drawMultilineText = (ctx, text6, left, top, maxWidth, lineHeight) => {
7627
+ const paragraphs = text6.split("\n");
7621
7628
  let currentY = top;
7622
7629
  paragraphs.forEach((paragraph, index) => {
7623
7630
  const words = paragraph.split("");
@@ -8774,6 +8781,1244 @@ var BoothConfigPage = ({ initialConfig, onSave }) => {
8774
8781
  )), /* @__PURE__ */ React69__default.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ React69__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__default.createElement("button", { className: "rounded border px-3 py-2", onClick: reset }, "\u6062\u590D\u9ED8\u8BA4")));
8775
8782
  };
8776
8783
 
8784
+ // src/mikuContest/logic/shared/defaults.ts
8785
+ var defaultMikuVotingRules = {
8786
+ maxVotesPerDay: 3,
8787
+ forbidDuplicateVotePerWork: true,
8788
+ maxVotesPerDevicePerDay: 20,
8789
+ maxVotesPerIpPerDay: 100
8790
+ };
8791
+ var createDefaultMikuContestConfig = (overrides) => ({
8792
+ id: overrides?.id || "miku-contest-default",
8793
+ name: overrides?.name || "\u521D\u97F3\u672A\u6765\u793E\u56E2\u5F81\u7A3F\u5927\u8D5B",
8794
+ theme: overrides?.theme || "\u521D\u97F3\u672A\u6765\u4E3B\u9898\u521B\u4F5C\u5F81\u7A3F",
8795
+ organizer: overrides?.organizer || "\u521D\u97F3\u672A\u6765\u793E\u56E2",
8796
+ awards: overrides?.awards || ["\u4E00\u7B49\u5956", "\u4E8C\u7B49\u5956", "\u4E09\u7B49\u5956", "\u4EBA\u6C14\u5956"],
8797
+ rules: overrides?.rules || "\u8BF7\u786E\u4FDD\u4F5C\u54C1\u539F\u521B\u4E14\u7B26\u5408\u793E\u56E2\u89C4\u8303\u3002",
8798
+ copyright: overrides?.copyright || "\u6295\u7A3F\u5373\u89C6\u4E3A\u6388\u6743\u8D5B\u4E8B\u5C55\u793A\u4E0E\u516C\u793A\u3002",
8799
+ timeline: overrides?.timeline || {
8800
+ submissionStartAt: (/* @__PURE__ */ new Date()).toISOString(),
8801
+ submissionEndAt: (/* @__PURE__ */ new Date()).toISOString(),
8802
+ votingStartAt: (/* @__PURE__ */ new Date()).toISOString(),
8803
+ votingEndAt: (/* @__PURE__ */ new Date()).toISOString(),
8804
+ publicResultAt: (/* @__PURE__ */ new Date()).toISOString()
8805
+ },
8806
+ votingRules: {
8807
+ ...defaultMikuVotingRules,
8808
+ ...overrides?.votingRules || {}
8809
+ },
8810
+ toggles: {
8811
+ submissionEnabled: overrides?.toggles?.submissionEnabled ?? true,
8812
+ votingEnabled: overrides?.toggles?.votingEnabled ?? true,
8813
+ resultEnabled: overrides?.toggles?.resultEnabled ?? false
8814
+ }
8815
+ });
8816
+
8817
+ // src/mikuContest/logic/shared/validators.ts
8818
+ var DESCRIPTION_LIMIT = 500;
8819
+ var MINIAPP_DESCRIPTION_LIMIT = 200;
8820
+ var TEXT_CONTENT_LIMIT = 2e3;
8821
+ var MAX_TAGS = 3;
8822
+ var hasValue = (value) => {
8823
+ return typeof value === "string" ? value.trim().length > 0 : value !== null && value !== void 0;
8824
+ };
8825
+ var validateByType = (type, input) => {
8826
+ const errors = [];
8827
+ const { content } = input;
8828
+ switch (type) {
8829
+ case "visual": {
8830
+ const imageCount = content.images?.length || 0;
8831
+ if (imageCount < 1 || imageCount > 3) {
8832
+ errors.push("\u89C6\u89C9\u7C7B\u4F5C\u54C1\u9700\u4E0A\u4F20 1-3 \u5F20\u56FE\u7247");
8833
+ }
8834
+ break;
8835
+ }
8836
+ case "video": {
8837
+ if (!hasValue(content.videoLink)) {
8838
+ errors.push("\u89C6\u9891\u7C7B\u4F5C\u54C1\u9700\u63D0\u4F9B\u89C6\u9891\u94FE\u63A5");
8839
+ }
8840
+ if (!hasValue(content.coverImage)) {
8841
+ errors.push("\u89C6\u9891\u7C7B\u4F5C\u54C1\u9700\u63D0\u4F9B\u5C01\u9762\u56FE");
8842
+ }
8843
+ break;
8844
+ }
8845
+ case "text": {
8846
+ const text6 = content.textContent || "";
8847
+ if (!text6.trim()) {
8848
+ errors.push("\u6587\u5B57\u7C7B\u4F5C\u54C1\u9700\u586B\u5199\u6B63\u6587");
8849
+ }
8850
+ if (text6.length > TEXT_CONTENT_LIMIT) {
8851
+ errors.push(`\u6587\u5B57\u6B63\u6587\u4E0D\u80FD\u8D85\u8FC7 ${TEXT_CONTENT_LIMIT} \u5B57`);
8852
+ }
8853
+ break;
8854
+ }
8855
+ case "audio": {
8856
+ if (!hasValue(content.audioLink)) {
8857
+ errors.push("\u97F3\u9891\u7C7B\u4F5C\u54C1\u9700\u63D0\u4F9B\u97F3\u9891\u94FE\u63A5");
8858
+ }
8859
+ break;
8860
+ }
8861
+ }
8862
+ return errors;
8863
+ };
8864
+ var validateMikuSubmissionInput = (input, mode = "web") => {
8865
+ const errors = [];
8866
+ if (!input.contestId.trim()) errors.push("contestId \u4E0D\u80FD\u4E3A\u7A7A");
8867
+ if (!input.authorId.trim()) errors.push("authorId \u4E0D\u80FD\u4E3A\u7A7A");
8868
+ if (!input.authorNickname.trim()) errors.push("\u4F5C\u8005\u6635\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
8869
+ if (!input.title.trim()) errors.push("\u4F5C\u54C1\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
8870
+ const descriptionLimit = mode === "miniapp" ? MINIAPP_DESCRIPTION_LIMIT : DESCRIPTION_LIMIT;
8871
+ if (input.description.length > descriptionLimit) {
8872
+ errors.push(`\u4F5C\u54C1\u7B80\u4ECB\u4E0D\u80FD\u8D85\u8FC7 ${descriptionLimit} \u5B57`);
8873
+ }
8874
+ if ((input.tags?.length || 0) > MAX_TAGS) {
8875
+ errors.push(`\u6807\u7B7E\u6700\u591A ${MAX_TAGS} \u4E2A`);
8876
+ }
8877
+ errors.push(...validateByType(input.type, input));
8878
+ return errors;
8879
+ };
8880
+
8881
+ // src/mikuContest/logic/shared/voting.ts
8882
+ var toVoteDayKey = (date = /* @__PURE__ */ new Date()) => {
8883
+ const y = date.getUTCFullYear();
8884
+ const m = String(date.getUTCMonth() + 1).padStart(2, "0");
8885
+ const d = String(date.getUTCDate()).padStart(2, "0");
8886
+ return `${y}-${m}-${d}`;
8887
+ };
8888
+ var checkVoteEligibility = (context) => {
8889
+ const { existingVotes, submissionId, voterId, dayKey, rules } = context;
8890
+ const userTodayVotes = existingVotes.filter((vote) => vote.voterId === voterId && vote.dayKey === dayKey);
8891
+ if (userTodayVotes.length >= rules.maxVotesPerDay) {
8892
+ return { ok: false, reason: "\u5DF2\u8FBE\u5230\u4ECA\u65E5\u6295\u7968\u4E0A\u9650" };
8893
+ }
8894
+ if (rules.forbidDuplicateVotePerWork) {
8895
+ const duplicated = userTodayVotes.some((vote) => vote.submissionId === submissionId);
8896
+ if (duplicated) return { ok: false, reason: "\u4E0D\u53EF\u91CD\u590D\u6295\u540C\u4E00\u4F5C\u54C1" };
8897
+ }
8898
+ return { ok: true };
8899
+ };
8900
+ var sortByVotesDesc = (items) => {
8901
+ return [...items].sort((a, b) => {
8902
+ if (b.voteCount === a.voteCount) {
8903
+ return (a.createdAt || "").localeCompare(b.createdAt || "");
8904
+ }
8905
+ return b.voteCount - a.voteCount;
8906
+ });
8907
+ };
8908
+ var randomId = (prefix) => {
8909
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
8910
+ };
8911
+ var serialNo = () => {
8912
+ const now = /* @__PURE__ */ new Date();
8913
+ const y = now.getFullYear();
8914
+ const m = String(now.getMonth() + 1).padStart(2, "0");
8915
+ const d = String(now.getDate()).padStart(2, "0");
8916
+ const seq = Math.floor(Math.random() * 9e3 + 1e3);
8917
+ return `MIKU-${y}${m}${d}-${seq}`;
8918
+ };
8919
+ var MikuContestService = class {
8920
+ constructor(options = {}) {
8921
+ this.submissions = /* @__PURE__ */ new Map();
8922
+ this.votes = [];
8923
+ this.announcements = /* @__PURE__ */ new Map();
8924
+ this.voterRestrictions = /* @__PURE__ */ new Map();
8925
+ this.contest = createDefaultMikuContestConfig(options.contestConfig);
8926
+ }
8927
+ getContestConfig() {
8928
+ return this.contest;
8929
+ }
8930
+ updateContestConfig(patch) {
8931
+ this.contest = {
8932
+ ...this.contest,
8933
+ ...patch,
8934
+ votingRules: {
8935
+ ...this.contest.votingRules,
8936
+ ...patch.votingRules || {}
8937
+ },
8938
+ toggles: {
8939
+ ...this.contest.toggles,
8940
+ ...patch.toggles || {}
8941
+ },
8942
+ timeline: {
8943
+ ...this.contest.timeline,
8944
+ ...patch.timeline || {}
8945
+ }
8946
+ };
8947
+ return this.contest;
8948
+ }
8949
+ createSubmission(input, mode = "web") {
8950
+ if (!this.contest.toggles.submissionEnabled) {
8951
+ throw new Error("\u5F53\u524D\u672A\u5F00\u653E\u6295\u7A3F");
8952
+ }
8953
+ const errors = validateMikuSubmissionInput(input, mode);
8954
+ if (errors.length > 0) {
8955
+ throw new Error(errors.join("\uFF1B"));
8956
+ }
8957
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8958
+ const next = {
8959
+ id: randomId("submission"),
8960
+ serialNo: serialNo(),
8961
+ contestId: input.contestId,
8962
+ authorId: input.authorId,
8963
+ authorNickname: input.authorNickname,
8964
+ title: input.title,
8965
+ type: input.type,
8966
+ description: input.description,
8967
+ tags: input.tags || [],
8968
+ content: input.content,
8969
+ voteCount: 0,
8970
+ status: "pending",
8971
+ createdAt: now,
8972
+ updatedAt: now
8973
+ };
8974
+ this.submissions.set(next.id, next);
8975
+ return next;
8976
+ }
8977
+ listSubmissions(filter) {
8978
+ const authorKeyword = filter?.authorKeyword?.trim().toLowerCase();
8979
+ const titleKeyword = filter?.titleKeyword?.trim().toLowerCase();
8980
+ return [...this.submissions.values()].filter((item) => {
8981
+ if (filter?.status && item.status !== filter.status) return false;
8982
+ if (filter?.type && item.type !== filter.type) return false;
8983
+ if (filter?.authorId && item.authorId !== filter.authorId) return false;
8984
+ if (authorKeyword && !item.authorNickname.toLowerCase().includes(authorKeyword)) return false;
8985
+ if (titleKeyword && !item.title.toLowerCase().includes(titleKeyword)) return false;
8986
+ return true;
8987
+ });
8988
+ }
8989
+ getSubmission(submissionId) {
8990
+ return this.submissions.get(submissionId) || null;
8991
+ }
8992
+ reviewSubmission(input) {
8993
+ const current = this.submissions.get(input.submissionId);
8994
+ if (!current) throw new Error("\u6295\u7A3F\u4E0D\u5B58\u5728");
8995
+ if (input.action === "reject" && !input.rejectReason?.trim()) {
8996
+ throw new Error("\u9A73\u56DE\u9700\u586B\u5199\u539F\u56E0");
8997
+ }
8998
+ const reviewed = {
8999
+ ...current,
9000
+ status: input.action === "approve" ? "approved" : "rejected",
9001
+ rejectReason: input.action === "reject" ? input.rejectReason?.trim() : void 0,
9002
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
9003
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9004
+ };
9005
+ this.submissions.set(reviewed.id, reviewed);
9006
+ return reviewed;
9007
+ }
9008
+ vote(input) {
9009
+ if (!this.contest.toggles.votingEnabled) {
9010
+ throw new Error("\u5F53\u524D\u672A\u5F00\u653E\u6295\u7968");
9011
+ }
9012
+ const restriction = this.voterRestrictions.get(input.voterId);
9013
+ if (restriction?.banned) {
9014
+ throw new Error("\u5F53\u524D\u8D26\u53F7\u5DF2\u88AB\u9650\u5236\u6295\u7968");
9015
+ }
9016
+ const target = this.submissions.get(input.submissionId);
9017
+ if (!target) throw new Error("\u4F5C\u54C1\u4E0D\u5B58\u5728");
9018
+ if (target.status !== "approved") throw new Error("\u4EC5\u53EF\u5BF9\u5DF2\u8FC7\u5BA1\u4F5C\u54C1\u6295\u7968");
9019
+ const dayKey = toVoteDayKey();
9020
+ const eligible = checkVoteEligibility({
9021
+ existingVotes: this.votes,
9022
+ submissionId: input.submissionId,
9023
+ voterId: input.voterId,
9024
+ dayKey,
9025
+ rules: this.contest.votingRules
9026
+ });
9027
+ if (!eligible.ok) {
9028
+ throw new Error(eligible.reason || "\u6295\u7968\u5931\u8D25");
9029
+ }
9030
+ const vote = {
9031
+ id: randomId("vote"),
9032
+ contestId: input.contestId,
9033
+ submissionId: input.submissionId,
9034
+ voterId: input.voterId,
9035
+ votedAt: (/* @__PURE__ */ new Date()).toISOString(),
9036
+ dayKey,
9037
+ deviceId: input.deviceId,
9038
+ ip: input.ip
9039
+ };
9040
+ this.votes.push(vote);
9041
+ const updated = {
9042
+ ...target,
9043
+ voteCount: target.voteCount + 1,
9044
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9045
+ };
9046
+ this.submissions.set(updated.id, updated);
9047
+ return updated;
9048
+ }
9049
+ getVoterRestriction(voterId) {
9050
+ return this.voterRestrictions.get(voterId) || null;
9051
+ }
9052
+ setVoterRestriction(input) {
9053
+ const next = {
9054
+ voterId: input.voterId,
9055
+ banned: input.banned,
9056
+ reason: input.reason,
9057
+ operatorId: input.operatorId,
9058
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9059
+ };
9060
+ this.voterRestrictions.set(input.voterId, next);
9061
+ return next;
9062
+ }
9063
+ resetVotes(input) {
9064
+ if (!input.submissionId && !input.voterId) {
9065
+ throw new Error("submissionId \u4E0E voterId \u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A");
9066
+ }
9067
+ const before = this.votes.length;
9068
+ const affected = /* @__PURE__ */ new Set();
9069
+ const remained = this.votes.filter((vote) => {
9070
+ const matchSubmission = input.submissionId ? vote.submissionId === input.submissionId : true;
9071
+ const matchVoter = input.voterId ? vote.voterId === input.voterId : true;
9072
+ const shouldRemove = matchSubmission && matchVoter;
9073
+ if (shouldRemove) affected.add(vote.submissionId);
9074
+ return !shouldRemove;
9075
+ });
9076
+ this.votes.length = 0;
9077
+ this.votes.push(...remained);
9078
+ this.recalculateVoteCounts();
9079
+ return {
9080
+ removedVotes: before - remained.length,
9081
+ affectedSubmissions: [...affected]
9082
+ };
9083
+ }
9084
+ listAnnouncements(contestId) {
9085
+ const all = [...this.announcements.values()];
9086
+ return contestId ? all.filter((item) => item.contestId === contestId) : all;
9087
+ }
9088
+ publishAnnouncement(input) {
9089
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9090
+ const announcement = {
9091
+ id: randomId("notice"),
9092
+ contestId: input.contestId,
9093
+ title: input.title,
9094
+ content: input.content,
9095
+ type: input.type,
9096
+ createdBy: input.createdBy,
9097
+ createdAt: now,
9098
+ updatedAt: now
9099
+ };
9100
+ this.announcements.set(announcement.id, announcement);
9101
+ return announcement;
9102
+ }
9103
+ getLeaderboard(limit = 10) {
9104
+ const ranked = sortByVotesDesc(this.listSubmissions({ status: "approved" })).slice(0, limit);
9105
+ return ranked.map((item, index) => ({
9106
+ submissionId: item.id,
9107
+ title: item.title,
9108
+ authorNickname: item.authorNickname,
9109
+ voteCount: item.voteCount,
9110
+ rank: index + 1
9111
+ }));
9112
+ }
9113
+ getSnapshot() {
9114
+ return {
9115
+ contest: this.contest,
9116
+ submissions: this.listSubmissions(),
9117
+ announcements: this.listAnnouncements(),
9118
+ leaderboard: this.getLeaderboard()
9119
+ };
9120
+ }
9121
+ getSubmissionExportRows(filter) {
9122
+ return this.listSubmissions(filter).map((item) => ({
9123
+ \u6295\u7A3F\u7F16\u53F7: item.serialNo,
9124
+ \u6295\u7A3FID: item.id,
9125
+ \u8D5B\u4E8BID: item.contestId,
9126
+ \u4F5C\u8005ID: item.authorId,
9127
+ \u4F5C\u8005\u6635\u79F0: item.authorNickname,
9128
+ \u4F5C\u54C1\u540D\u79F0: item.title,
9129
+ \u4F5C\u54C1\u7C7B\u578B: item.type,
9130
+ \u7B80\u4ECB: item.description,
9131
+ \u6807\u7B7E: item.tags.join(","),
9132
+ \u5BA1\u6838\u72B6\u6001: item.status,
9133
+ \u9A73\u56DE\u539F\u56E0: item.rejectReason || "",
9134
+ \u7968\u6570: item.voteCount,
9135
+ \u63D0\u4EA4\u65F6\u95F4: item.createdAt,
9136
+ \u66F4\u65B0\u65F6\u95F4: item.updatedAt
9137
+ }));
9138
+ }
9139
+ exportSubmissionExcel(filter) {
9140
+ const rows = this.getSubmissionExportRows(filter);
9141
+ const workbook = XLSX.utils.book_new();
9142
+ const worksheet = XLSX.utils.json_to_sheet(rows);
9143
+ XLSX.utils.book_append_sheet(workbook, worksheet, "submissions");
9144
+ return XLSX.write(workbook, { bookType: "xlsx", type: "buffer" });
9145
+ }
9146
+ recalculateVoteCounts() {
9147
+ const counts = /* @__PURE__ */ new Map();
9148
+ for (const vote of this.votes) {
9149
+ counts.set(vote.submissionId, (counts.get(vote.submissionId) || 0) + 1);
9150
+ }
9151
+ for (const [id, submission] of this.submissions.entries()) {
9152
+ const nextCount = counts.get(id) || 0;
9153
+ if (submission.voteCount === nextCount) continue;
9154
+ this.submissions.set(id, {
9155
+ ...submission,
9156
+ voteCount: nextCount,
9157
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9158
+ });
9159
+ }
9160
+ }
9161
+ exportPersistenceState() {
9162
+ return {
9163
+ contest: this.contest,
9164
+ submissions: [...this.submissions.values()],
9165
+ votes: [...this.votes],
9166
+ announcements: [...this.announcements.values()],
9167
+ voterRestrictions: [...this.voterRestrictions.values()],
9168
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9169
+ };
9170
+ }
9171
+ importPersistenceState(state) {
9172
+ this.contest = state.contest;
9173
+ this.submissions.clear();
9174
+ this.announcements.clear();
9175
+ this.voterRestrictions.clear();
9176
+ this.votes.length = 0;
9177
+ for (const item of state.submissions) {
9178
+ this.submissions.set(item.id, item);
9179
+ }
9180
+ for (const item of state.announcements) {
9181
+ this.announcements.set(item.id, item);
9182
+ }
9183
+ for (const item of state.voterRestrictions) {
9184
+ this.voterRestrictions.set(item.voterId, item);
9185
+ }
9186
+ this.votes.push(...state.votes);
9187
+ }
9188
+ };
9189
+ var createMikuContestService = (options) => {
9190
+ return new MikuContestService(options);
9191
+ };
9192
+ var mikuContestConfigs = pgTable("miku_contest_configs", {
9193
+ contestId: text("contest_id").primaryKey(),
9194
+ config: jsonb("config").$type().notNull(),
9195
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
9196
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
9197
+ });
9198
+ var mikuContestSubmissions = pgTable("miku_contest_submissions", {
9199
+ id: text("id").primaryKey(),
9200
+ contestId: text("contest_id").notNull(),
9201
+ serialNo: text("serial_no").notNull(),
9202
+ authorId: text("author_id").notNull(),
9203
+ authorNickname: text("author_nickname").notNull(),
9204
+ title: text("title").notNull(),
9205
+ type: text("type").notNull(),
9206
+ description: text("description").notNull(),
9207
+ tags: jsonb("tags").$type().notNull(),
9208
+ content: jsonb("content").$type().notNull(),
9209
+ voteCount: integer("vote_count").notNull().default(0),
9210
+ status: text("status").notNull(),
9211
+ rejectReason: text("reject_reason"),
9212
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
9213
+ reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
9214
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull()
9215
+ });
9216
+ var mikuContestVotes = pgTable("miku_contest_votes", {
9217
+ id: text("id").primaryKey(),
9218
+ contestId: text("contest_id").notNull(),
9219
+ submissionId: text("submission_id").notNull(),
9220
+ voterId: text("voter_id").notNull(),
9221
+ votedAt: text("voted_at").notNull(),
9222
+ dayKey: text("day_key").notNull(),
9223
+ deviceId: text("device_id"),
9224
+ ip: text("ip")
9225
+ });
9226
+ var mikuContestNotices = pgTable("miku_contest_notices", {
9227
+ id: text("id").primaryKey(),
9228
+ contestId: text("contest_id").notNull(),
9229
+ title: text("title").notNull(),
9230
+ content: text("content").notNull(),
9231
+ type: text("type").notNull(),
9232
+ createdBy: text("created_by").notNull(),
9233
+ createdAt: text("created_at").notNull(),
9234
+ updatedAt: text("updated_at").notNull()
9235
+ });
9236
+ var mikuContestVoterRestrictions = pgTable("miku_contest_voter_restrictions", {
9237
+ id: text("id").primaryKey(),
9238
+ contestId: text("contest_id").notNull(),
9239
+ data: jsonb("data").$type().notNull()
9240
+ });
9241
+ var MikuContestStateDbService = class {
9242
+ constructor(db) {
9243
+ this.db = db;
9244
+ }
9245
+ async loadState(contestId) {
9246
+ const configRows = await this.db.select({ config: mikuContestConfigs.config }).from(mikuContestConfigs).where(eq(mikuContestConfigs.contestId, contestId)).limit(1);
9247
+ const config = configRows[0]?.config;
9248
+ if (!config) return null;
9249
+ const [submissions, votes, announcements, restrictions] = await Promise.all([
9250
+ this.db.select().from(mikuContestSubmissions).where(eq(mikuContestSubmissions.contestId, contestId)),
9251
+ this.db.select().from(mikuContestVotes).where(eq(mikuContestVotes.contestId, contestId)),
9252
+ this.db.select().from(mikuContestNotices).where(eq(mikuContestNotices.contestId, contestId)),
9253
+ this.db.select({ data: mikuContestVoterRestrictions.data }).from(mikuContestVoterRestrictions).where(eq(mikuContestVoterRestrictions.contestId, contestId))
9254
+ ]);
9255
+ return {
9256
+ contest: config,
9257
+ submissions,
9258
+ votes,
9259
+ announcements,
9260
+ voterRestrictions: restrictions.map((item) => item.data),
9261
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9262
+ };
9263
+ }
9264
+ async saveState(state) {
9265
+ const contestId = state.contest.id;
9266
+ const exists = await this.db.select({ contestId: mikuContestConfigs.contestId }).from(mikuContestConfigs).where(eq(mikuContestConfigs.contestId, contestId)).limit(1);
9267
+ if (exists[0]) {
9268
+ await this.db.update(mikuContestConfigs).set({
9269
+ config: state.contest,
9270
+ updatedAt: /* @__PURE__ */ new Date()
9271
+ }).where(eq(mikuContestConfigs.contestId, contestId));
9272
+ } else {
9273
+ await this.db.insert(mikuContestConfigs).values({
9274
+ contestId,
9275
+ config: state.contest
9276
+ });
9277
+ }
9278
+ await this.db.delete(mikuContestSubmissions).where(eq(mikuContestSubmissions.contestId, contestId));
9279
+ await this.db.delete(mikuContestVotes).where(eq(mikuContestVotes.contestId, contestId));
9280
+ await this.db.delete(mikuContestNotices).where(eq(mikuContestNotices.contestId, contestId));
9281
+ await this.db.delete(mikuContestVoterRestrictions).where(eq(mikuContestVoterRestrictions.contestId, contestId));
9282
+ if (state.submissions.length > 0) {
9283
+ await this.db.insert(mikuContestSubmissions).values(
9284
+ state.submissions.map((item) => ({
9285
+ ...item,
9286
+ contestId,
9287
+ createdAt: new Date(item.createdAt),
9288
+ reviewedAt: item.reviewedAt ? new Date(item.reviewedAt) : null,
9289
+ updatedAt: new Date(item.updatedAt)
9290
+ }))
9291
+ );
9292
+ }
9293
+ if (state.votes.length > 0) {
9294
+ await this.db.insert(mikuContestVotes).values(state.votes);
9295
+ }
9296
+ if (state.announcements.length > 0) {
9297
+ await this.db.insert(mikuContestNotices).values(state.announcements);
9298
+ }
9299
+ if (state.voterRestrictions.length > 0) {
9300
+ await this.db.insert(mikuContestVoterRestrictions).values(
9301
+ state.voterRestrictions.map((item) => ({
9302
+ id: `${contestId}:${item.voterId}`,
9303
+ contestId,
9304
+ data: item
9305
+ }))
9306
+ );
9307
+ }
9308
+ }
9309
+ };
9310
+
9311
+ // src/mikuContest/server/persistence/drizzle-adapter.ts
9312
+ var createMikuContestDrizzlePersistenceAdapter = (db) => {
9313
+ const service = new MikuContestStateDbService(db);
9314
+ return {
9315
+ loadState: (contestId) => service.loadState(contestId),
9316
+ saveState: (state) => service.saveState(state)
9317
+ };
9318
+ };
9319
+
9320
+ // src/mikuContest/server/persistence/service.ts
9321
+ var MikuContestPersistentService = class {
9322
+ constructor(options) {
9323
+ this.options = options;
9324
+ this.hydrated = false;
9325
+ this.hydrationPromise = null;
9326
+ this.engine = createMikuContestService(options);
9327
+ }
9328
+ async ensureHydrated() {
9329
+ if (this.hydrated) return;
9330
+ if (this.hydrationPromise) return this.hydrationPromise;
9331
+ this.hydrationPromise = (async () => {
9332
+ const contestId = this.engine.getContestConfig().id;
9333
+ const loaded = await this.options.persistenceAdapter.loadState(contestId);
9334
+ if (loaded) {
9335
+ this.engine.importPersistenceState(loaded);
9336
+ } else {
9337
+ await this.options.persistenceAdapter.saveState(this.engine.exportPersistenceState());
9338
+ }
9339
+ this.hydrated = true;
9340
+ })();
9341
+ await this.hydrationPromise;
9342
+ }
9343
+ async persist() {
9344
+ await this.options.persistenceAdapter.saveState(this.engine.exportPersistenceState());
9345
+ }
9346
+ async getContestConfig() {
9347
+ await this.ensureHydrated();
9348
+ return this.engine.getContestConfig();
9349
+ }
9350
+ async updateContestConfig(patch) {
9351
+ await this.ensureHydrated();
9352
+ const data = this.engine.updateContestConfig(patch);
9353
+ await this.persist();
9354
+ return data;
9355
+ }
9356
+ async createSubmission(input, mode = "web") {
9357
+ await this.ensureHydrated();
9358
+ const data = this.engine.createSubmission(input, mode);
9359
+ await this.persist();
9360
+ return data;
9361
+ }
9362
+ async listSubmissions(filter) {
9363
+ await this.ensureHydrated();
9364
+ return this.engine.listSubmissions(filter);
9365
+ }
9366
+ async getSubmission(submissionId) {
9367
+ await this.ensureHydrated();
9368
+ return this.engine.getSubmission(submissionId);
9369
+ }
9370
+ async reviewSubmission(input) {
9371
+ await this.ensureHydrated();
9372
+ const data = this.engine.reviewSubmission(input);
9373
+ await this.persist();
9374
+ return data;
9375
+ }
9376
+ async vote(input) {
9377
+ await this.ensureHydrated();
9378
+ const data = this.engine.vote(input);
9379
+ await this.persist();
9380
+ return data;
9381
+ }
9382
+ async getVoterRestriction(voterId) {
9383
+ await this.ensureHydrated();
9384
+ return this.engine.getVoterRestriction(voterId);
9385
+ }
9386
+ async setVoterRestriction(input) {
9387
+ await this.ensureHydrated();
9388
+ const data = this.engine.setVoterRestriction(input);
9389
+ await this.persist();
9390
+ return data;
9391
+ }
9392
+ async resetVotes(input) {
9393
+ await this.ensureHydrated();
9394
+ const data = this.engine.resetVotes(input);
9395
+ await this.persist();
9396
+ return data;
9397
+ }
9398
+ async listAnnouncements(contestId) {
9399
+ await this.ensureHydrated();
9400
+ return this.engine.listAnnouncements(contestId);
9401
+ }
9402
+ async publishAnnouncement(input) {
9403
+ await this.ensureHydrated();
9404
+ const data = this.engine.publishAnnouncement(input);
9405
+ await this.persist();
9406
+ return data;
9407
+ }
9408
+ async getLeaderboard(limit = 10) {
9409
+ await this.ensureHydrated();
9410
+ return this.engine.getLeaderboard(limit);
9411
+ }
9412
+ async getSnapshot() {
9413
+ await this.ensureHydrated();
9414
+ return this.engine.getSnapshot();
9415
+ }
9416
+ async getSubmissionExportRows(filter) {
9417
+ await this.ensureHydrated();
9418
+ return this.engine.getSubmissionExportRows(filter);
9419
+ }
9420
+ async exportSubmissionExcel(filter) {
9421
+ await this.ensureHydrated();
9422
+ return this.engine.exportSubmissionExcel(filter);
9423
+ }
9424
+ };
9425
+ var createMikuContestPersistentService = (options) => {
9426
+ return new MikuContestPersistentService(options);
9427
+ };
9428
+
9429
+ // src/mikuContest/server/db.ts
9430
+ var MikuContestDbService = class {
9431
+ constructor() {
9432
+ this._db = null;
9433
+ }
9434
+ setDb(db) {
9435
+ this._db = db;
9436
+ }
9437
+ isConfigured() {
9438
+ return Boolean(this._db);
9439
+ }
9440
+ get db() {
9441
+ if (!this._db) {
9442
+ throw new Error("MikuContestDbService: Database instance not set. Call setDb(db) first.");
9443
+ }
9444
+ return this._db;
9445
+ }
9446
+ };
9447
+ var mikuContestDbService = new MikuContestDbService();
9448
+
9449
+ // src/mikuContest/logic/hooks/useMikuContest.ts
9450
+ var useMikuContest = (options) => {
9451
+ const [service] = useState(() => createMikuContestService(options));
9452
+ const [version, setVersion] = useState(0);
9453
+ const refresh = () => setVersion((value) => value + 1);
9454
+ const snapshot = useMemo(() => {
9455
+ return service.getSnapshot();
9456
+ }, [service, version]);
9457
+ return {
9458
+ service,
9459
+ snapshot,
9460
+ refresh
9461
+ };
9462
+ };
9463
+
9464
+ // src/mikuContest/service/api/client.ts
9465
+ var toQueryString = (filter) => {
9466
+ if (!filter) return "";
9467
+ const params = new URLSearchParams();
9468
+ if (filter.status) params.set("status", filter.status);
9469
+ if (filter.type) params.set("type", filter.type);
9470
+ if (filter.authorId) params.set("authorId", filter.authorId);
9471
+ if (filter.authorKeyword) params.set("authorKeyword", filter.authorKeyword);
9472
+ if (filter.titleKeyword) params.set("titleKeyword", filter.titleKeyword);
9473
+ const query = params.toString();
9474
+ return query ? `?${query}` : "";
9475
+ };
9476
+ var unwrap = (result) => {
9477
+ if (!result.success || result.data === void 0) {
9478
+ throw new Error(result.error || "\u8BF7\u6C42\u5931\u8D25");
9479
+ }
9480
+ return result.data;
9481
+ };
9482
+ var createMikuContestApiClient = (basePath, requester) => {
9483
+ return {
9484
+ async getSnapshot() {
9485
+ const result = await requester(`${basePath}/contest`, { method: "GET" });
9486
+ return unwrap(result);
9487
+ },
9488
+ async updateContestConfig(patch) {
9489
+ const result = await requester(`${basePath}/contest`, {
9490
+ method: "PATCH",
9491
+ body: patch
9492
+ });
9493
+ return unwrap(result);
9494
+ },
9495
+ async createSubmission(input, mode = "web") {
9496
+ const result = await requester(`${basePath}/submissions`, {
9497
+ method: "POST",
9498
+ body: { payload: input, mode }
9499
+ });
9500
+ return unwrap(result);
9501
+ },
9502
+ async listSubmissions(filter) {
9503
+ const result = await requester(
9504
+ `${basePath}/submissions${toQueryString(filter)}`,
9505
+ { method: "GET" }
9506
+ );
9507
+ return unwrap(result);
9508
+ },
9509
+ async reviewSubmission(input) {
9510
+ const result = await requester(`${basePath}/submissions/review`, {
9511
+ method: "POST",
9512
+ body: input
9513
+ });
9514
+ return unwrap(result);
9515
+ },
9516
+ async vote(input) {
9517
+ const result = await requester(`${basePath}/votes`, {
9518
+ method: "POST",
9519
+ body: input
9520
+ });
9521
+ return unwrap(result);
9522
+ },
9523
+ async setVoterRestriction(input) {
9524
+ const result = await requester(`${basePath}/admin/voter-restrictions`, {
9525
+ method: "POST",
9526
+ body: input
9527
+ });
9528
+ return unwrap(result);
9529
+ },
9530
+ async resetVotes(input) {
9531
+ const result = await requester(
9532
+ `${basePath}/admin/votes/reset`,
9533
+ {
9534
+ method: "POST",
9535
+ body: input
9536
+ }
9537
+ );
9538
+ return unwrap(result);
9539
+ },
9540
+ async exportSubmissions(filter) {
9541
+ const response = await fetch(`${basePath}/admin/submissions/export${toQueryString(filter)}`);
9542
+ if (!response.ok) {
9543
+ throw new Error(`\u5BFC\u51FA\u5931\u8D25: ${response.status}`);
9544
+ }
9545
+ return response.arrayBuffer();
9546
+ }
9547
+ };
9548
+ };
9549
+
9550
+ // src/mikuContest/service/web/index.ts
9551
+ var web_exports = {};
9552
+ __export(web_exports, {
9553
+ createMikuContestWebClient: () => createMikuContestWebClient
9554
+ });
9555
+
9556
+ // src/mikuContest/service/web/client.ts
9557
+ var defaultRequester = (options) => {
9558
+ const baseUrl = options.baseUrl || "";
9559
+ const commonHeaders = options.headers || {};
9560
+ return async (url, requestOptions) => {
9561
+ const response = await fetch(`${baseUrl}${url}`, {
9562
+ method: requestOptions?.method || "GET",
9563
+ headers: {
9564
+ "Content-Type": "application/json",
9565
+ ...commonHeaders
9566
+ },
9567
+ body: requestOptions?.body ? JSON.stringify(requestOptions.body) : void 0
9568
+ });
9569
+ const json = await response.json();
9570
+ return json;
9571
+ };
9572
+ };
9573
+ var createMikuContestWebClient = (options = {}) => {
9574
+ const basePath = options.basePath || "/api/miku-contest";
9575
+ return createMikuContestApiClient(basePath, defaultRequester(options));
9576
+ };
9577
+
9578
+ // src/mikuContest/service/miniapp/index.ts
9579
+ var miniapp_exports = {};
9580
+ __export(miniapp_exports, {
9581
+ createMikuContestMiniappClient: () => createMikuContestMiniappClient
9582
+ });
9583
+
9584
+ // src/mikuContest/service/miniapp/client.ts
9585
+ var createMikuContestMiniappClient = (options) => {
9586
+ const basePath = options.basePath || "/api/miku-contest";
9587
+ return createMikuContestApiClient(basePath, options.requester);
9588
+ };
9589
+ var isDrizzleDb = (value) => {
9590
+ if (!value || typeof value !== "object") return false;
9591
+ const candidate = value;
9592
+ return typeof candidate.select === "function" && typeof candidate.insert === "function" && typeof candidate.update === "function" && typeof candidate.delete === "function";
9593
+ };
9594
+ var resolveService = (config) => {
9595
+ if (config?.service) return config.service;
9596
+ const adapter = config?.persistenceAdapter || (isDrizzleDb(config?.db) ? createMikuContestDrizzlePersistenceAdapter(config.db) : null);
9597
+ if (adapter) {
9598
+ return createMikuContestPersistentService({
9599
+ persistenceAdapter: adapter
9600
+ });
9601
+ }
9602
+ return createMikuContestService();
9603
+ };
9604
+ var createGetContestSnapshotHandler = (config) => {
9605
+ const service = resolveService(config);
9606
+ return async (_request) => {
9607
+ const data = await service.getSnapshot();
9608
+ return NextResponse.json({ success: true, data });
9609
+ };
9610
+ };
9611
+ var createUpdateContestConfigHandler = (config) => {
9612
+ const service = resolveService(config);
9613
+ return async (request) => {
9614
+ try {
9615
+ const payload = await request.json();
9616
+ const data = await service.updateContestConfig(payload);
9617
+ return NextResponse.json({ success: true, data });
9618
+ } catch (error) {
9619
+ return NextResponse.json({ success: false, error: error.message }, { status: 400 });
9620
+ }
9621
+ };
9622
+ };
9623
+ var createCreateSubmissionHandler = (config) => {
9624
+ const service = resolveService(config);
9625
+ return async (request) => {
9626
+ try {
9627
+ const body = await request.json();
9628
+ const mode = body.mode || "web";
9629
+ const payload = body.payload;
9630
+ if (!payload) {
9631
+ return NextResponse.json({ success: false, error: "payload \u4E0D\u80FD\u4E3A\u7A7A" }, { status: 400 });
9632
+ }
9633
+ const data = await service.createSubmission(payload, mode);
9634
+ return NextResponse.json({ success: true, data });
9635
+ } catch (error) {
9636
+ return NextResponse.json({ success: false, error: error.message }, { status: 400 });
9637
+ }
9638
+ };
9639
+ };
9640
+ var createVoteHandler = (config) => {
9641
+ const service = resolveService(config);
9642
+ return async (request) => {
9643
+ try {
9644
+ const payload = await request.json();
9645
+ const data = await service.vote(payload);
9646
+ return NextResponse.json({ success: true, data });
9647
+ } catch (error) {
9648
+ return NextResponse.json({ success: false, error: error.message }, { status: 400 });
9649
+ }
9650
+ };
9651
+ };
9652
+ var createReviewSubmissionHandler = (config) => {
9653
+ const service = resolveService(config);
9654
+ return async (request) => {
9655
+ try {
9656
+ const payload = await request.json();
9657
+ const data = await service.reviewSubmission(payload);
9658
+ return NextResponse.json({ success: true, data });
9659
+ } catch (error) {
9660
+ return NextResponse.json({ success: false, error: error.message }, { status: 400 });
9661
+ }
9662
+ };
9663
+ };
9664
+ var buildSubmissionFilterFromQuery = (request) => {
9665
+ const search = request.nextUrl.searchParams;
9666
+ const status = search.get("status");
9667
+ const type = search.get("type");
9668
+ return {
9669
+ status: status ? status : void 0,
9670
+ type: type ? type : void 0,
9671
+ authorId: search.get("authorId") || void 0,
9672
+ authorKeyword: search.get("authorKeyword") || void 0,
9673
+ titleKeyword: search.get("titleKeyword") || void 0
9674
+ };
9675
+ };
9676
+ var createListSubmissionsHandler = (config) => {
9677
+ const service = resolveService(config);
9678
+ return async (request) => {
9679
+ const filter = buildSubmissionFilterFromQuery(request);
9680
+ const data = await service.listSubmissions(filter);
9681
+ return NextResponse.json({ success: true, data });
9682
+ };
9683
+ };
9684
+ var createSetVoterRestrictionHandler = (config) => {
9685
+ const service = resolveService(config);
9686
+ return async (request) => {
9687
+ try {
9688
+ const payload = await request.json();
9689
+ const data = await service.setVoterRestriction(payload);
9690
+ return NextResponse.json({ success: true, data });
9691
+ } catch (error) {
9692
+ return NextResponse.json({ success: false, error: error.message }, { status: 400 });
9693
+ }
9694
+ };
9695
+ };
9696
+ var createResetVotesHandler = (config) => {
9697
+ const service = resolveService(config);
9698
+ return async (request) => {
9699
+ try {
9700
+ const payload = await request.json();
9701
+ const data = await service.resetVotes(payload);
9702
+ return NextResponse.json({ success: true, data });
9703
+ } catch (error) {
9704
+ return NextResponse.json({ success: false, error: error.message }, { status: 400 });
9705
+ }
9706
+ };
9707
+ };
9708
+ var createExportSubmissionsHandler = (config) => {
9709
+ const service = resolveService(config);
9710
+ return async (request) => {
9711
+ const filter = buildSubmissionFilterFromQuery(request);
9712
+ const data = await service.exportSubmissionExcel(filter);
9713
+ const body = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
9714
+ return new NextResponse(body, {
9715
+ status: 200,
9716
+ headers: {
9717
+ "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
9718
+ "Content-Disposition": 'attachment; filename="miku-submissions.xlsx"'
9719
+ }
9720
+ });
9721
+ };
9722
+ };
9723
+
9724
+ // src/mikuContest/ui/web/index.ts
9725
+ var web_exports2 = {};
9726
+ __export(web_exports2, {
9727
+ MikuContestAdminPage: () => MikuContestAdminPage_default,
9728
+ MikuContestArtistPage: () => MikuContestArtistPage_default,
9729
+ MikuContestAudiencePage: () => MikuContestAudiencePage_default,
9730
+ MikuContestDashboard: () => MikuContestDashboard_default,
9731
+ MikuContestPage: () => MikuContestPage_default
9732
+ });
9733
+ var MikuContestDashboard = ({ snapshot }) => {
9734
+ return /* @__PURE__ */ React69__default.createElement("div", null, /* @__PURE__ */ React69__default.createElement("h2", null, snapshot.contest.name), /* @__PURE__ */ React69__default.createElement("p", null, snapshot.contest.theme), /* @__PURE__ */ React69__default.createElement("p", null, "\u6295\u7A3F\u6570\uFF1A", snapshot.submissions.length), /* @__PURE__ */ React69__default.createElement("p", null, "\u516C\u544A\u6570\uFF1A", snapshot.announcements.length), /* @__PURE__ */ React69__default.createElement("ul", null, snapshot.leaderboard.map((item) => /* @__PURE__ */ React69__default.createElement("li", { key: item.submissionId }, "#", item.rank, " ", item.title, " - ", item.voteCount, "\u7968"))));
9735
+ };
9736
+ var MikuContestDashboard_default = MikuContestDashboard;
9737
+ var MikuContestAudiencePage = ({
9738
+ client,
9739
+ voterId,
9740
+ title = "\u89C2\u4F17\u6295\u7968\u533A"
9741
+ }) => {
9742
+ const api = useMemo(() => client || createMikuContestWebClient(), [client]);
9743
+ const [snapshot, setSnapshot] = useState(null);
9744
+ const [loading, setLoading] = useState(false);
9745
+ const [error, setError] = useState(null);
9746
+ const approvedWorks = useMemo(() => {
9747
+ if (!snapshot) return [];
9748
+ return snapshot.submissions.filter((item) => item.status === "approved");
9749
+ }, [snapshot]);
9750
+ const loadSnapshot = async () => {
9751
+ setLoading(true);
9752
+ setError(null);
9753
+ try {
9754
+ const data = await api.getSnapshot();
9755
+ setSnapshot(data);
9756
+ } catch (e) {
9757
+ setError(e.message);
9758
+ } finally {
9759
+ setLoading(false);
9760
+ }
9761
+ };
9762
+ useEffect(() => {
9763
+ void loadSnapshot();
9764
+ }, []);
9765
+ const handleVote = async (submission) => {
9766
+ if (!snapshot) return;
9767
+ try {
9768
+ await api.vote({
9769
+ contestId: snapshot.contest.id,
9770
+ submissionId: submission.id,
9771
+ voterId
9772
+ });
9773
+ await loadSnapshot();
9774
+ } catch (e) {
9775
+ setError(e.message);
9776
+ }
9777
+ };
9778
+ return /* @__PURE__ */ React69__default.createElement("section", null, /* @__PURE__ */ React69__default.createElement("h2", null, title), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => void loadSnapshot(), disabled: loading }, loading ? "\u5237\u65B0\u4E2D..." : "\u5237\u65B0\u6570\u636E"), error ? /* @__PURE__ */ React69__default.createElement("p", { style: { color: "crimson" } }, "\u9519\u8BEF\uFF1A", error) : null, !snapshot ? null : /* @__PURE__ */ React69__default.createElement(React69__default.Fragment, null, /* @__PURE__ */ React69__default.createElement("p", null, "\u8D5B\u4E8B\uFF1A", snapshot.contest.name, "\uFF5C\u4E3B\u9898\uFF1A", snapshot.contest.theme), /* @__PURE__ */ React69__default.createElement("p", null, "\u5DF2\u8FC7\u5BA1\u4F5C\u54C1\uFF1A", approvedWorks.length, "\uFF5C\u6BCF\u65E5\u4E0A\u9650\uFF1A", snapshot.contest.votingRules.maxVotesPerDay), /* @__PURE__ */ React69__default.createElement("ul", null, approvedWorks.map((work) => /* @__PURE__ */ React69__default.createElement("li", { key: work.id }, /* @__PURE__ */ React69__default.createElement("strong", null, work.title), "\uFF08", work.authorNickname, "\uFF09- \u5F53\u524D ", work.voteCount, " \u7968", " ", /* @__PURE__ */ React69__default.createElement("button", { onClick: () => void handleVote(work) }, "\u6295\u7968"))))));
9779
+ };
9780
+ var MikuContestAudiencePage_default = MikuContestAudiencePage;
9781
+ var workTypes = ["visual", "video", "text", "audio"];
9782
+ var MikuContestArtistPage = ({
9783
+ client,
9784
+ authorId,
9785
+ authorNickname,
9786
+ title = "\u753B\u5E08\u6295\u7A3F\u533A"
9787
+ }) => {
9788
+ const api = useMemo(() => client || createMikuContestWebClient(), [client]);
9789
+ const [snapshot, setSnapshot] = useState(null);
9790
+ const [mySubmissions, setMySubmissions] = useState([]);
9791
+ const [submitting, setSubmitting] = useState(false);
9792
+ const [loading, setLoading] = useState(false);
9793
+ const [error, setError] = useState(null);
9794
+ const [titleInput, setTitleInput] = useState("");
9795
+ const [descInput, setDescInput] = useState("");
9796
+ const [coverImage, setCoverImage] = useState("");
9797
+ const [workType, setWorkType] = useState("visual");
9798
+ const loadData = async () => {
9799
+ setLoading(true);
9800
+ setError(null);
9801
+ try {
9802
+ const [contest, mine] = await Promise.all([
9803
+ api.getSnapshot(),
9804
+ api.listSubmissions({ authorId })
9805
+ ]);
9806
+ setSnapshot(contest);
9807
+ setMySubmissions(mine);
9808
+ } catch (e) {
9809
+ setError(e.message);
9810
+ } finally {
9811
+ setLoading(false);
9812
+ }
9813
+ };
9814
+ useEffect(() => {
9815
+ void loadData();
9816
+ }, []);
9817
+ const submitWork = async () => {
9818
+ if (!snapshot) return;
9819
+ const payload = {
9820
+ contestId: snapshot.contest.id,
9821
+ authorId,
9822
+ authorNickname,
9823
+ title: titleInput,
9824
+ description: descInput,
9825
+ type: workType,
9826
+ tags: ["web"],
9827
+ content: {
9828
+ coverImage,
9829
+ images: coverImage ? [coverImage] : void 0
9830
+ }
9831
+ };
9832
+ setSubmitting(true);
9833
+ setError(null);
9834
+ try {
9835
+ await api.createSubmission(payload, "web");
9836
+ setTitleInput("");
9837
+ setDescInput("");
9838
+ setCoverImage("");
9839
+ await loadData();
9840
+ } catch (e) {
9841
+ setError(e.message);
9842
+ } finally {
9843
+ setSubmitting(false);
9844
+ }
9845
+ };
9846
+ return /* @__PURE__ */ React69__default.createElement("section", null, /* @__PURE__ */ React69__default.createElement("h2", null, title), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => void loadData(), disabled: loading }, loading ? "\u5237\u65B0\u4E2D..." : "\u5237\u65B0\u6570\u636E"), error ? /* @__PURE__ */ React69__default.createElement("p", { style: { color: "crimson" } }, "\u9519\u8BEF\uFF1A", error) : null, /* @__PURE__ */ React69__default.createElement("div", null, /* @__PURE__ */ React69__default.createElement("h3", null, "\u65B0\u5EFA\u6295\u7A3F"), /* @__PURE__ */ React69__default.createElement("input", { value: titleInput, onChange: (e) => setTitleInput(e.target.value), placeholder: "\u4F5C\u54C1\u6807\u9898" }), /* @__PURE__ */ React69__default.createElement("br", null), /* @__PURE__ */ React69__default.createElement("textarea", { value: descInput, onChange: (e) => setDescInput(e.target.value), placeholder: "\u4F5C\u54C1\u7B80\u4ECB" }), /* @__PURE__ */ React69__default.createElement("br", null), /* @__PURE__ */ React69__default.createElement("input", { value: coverImage, onChange: (e) => setCoverImage(e.target.value), placeholder: "\u5C01\u9762 URL" }), /* @__PURE__ */ React69__default.createElement("br", null), /* @__PURE__ */ React69__default.createElement("select", { value: workType, onChange: (e) => setWorkType(e.target.value) }, workTypes.map((item) => /* @__PURE__ */ React69__default.createElement("option", { value: item, key: item }, item))), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => void submitWork(), disabled: submitting || !snapshot }, submitting ? "\u63D0\u4EA4\u4E2D..." : "\u63D0\u4EA4\u7A3F\u4EF6")), /* @__PURE__ */ React69__default.createElement("div", null, /* @__PURE__ */ React69__default.createElement("h3", null, "\u6211\u7684\u6295\u7A3F\uFF08", mySubmissions.length, "\uFF09"), /* @__PURE__ */ React69__default.createElement("ul", null, mySubmissions.map((item) => /* @__PURE__ */ React69__default.createElement("li", { key: item.id }, item.title, "\uFF5C\u72B6\u6001\uFF1A", item.status, "\uFF5C\u7968\u6570\uFF1A", item.voteCount, item.rejectReason ? `\uFF5C\u9A73\u56DE\uFF1A${item.rejectReason}` : "")))));
9847
+ };
9848
+ var MikuContestArtistPage_default = MikuContestArtistPage;
9849
+ var MikuContestAdminPage = ({
9850
+ client,
9851
+ adminId,
9852
+ title = "\u7BA1\u7406\u5458\u9762\u677F"
9853
+ }) => {
9854
+ const api = useMemo(() => client || createMikuContestWebClient(), [client]);
9855
+ const [snapshot, setSnapshot] = useState(null);
9856
+ const [submissions, setSubmissions] = useState([]);
9857
+ const [loading, setLoading] = useState(false);
9858
+ const [error, setError] = useState(null);
9859
+ const [voterId, setVoterId] = useState("");
9860
+ const loadData = async () => {
9861
+ setLoading(true);
9862
+ setError(null);
9863
+ try {
9864
+ const [contest, list] = await Promise.all([api.getSnapshot(), api.listSubmissions()]);
9865
+ setSnapshot(contest);
9866
+ setSubmissions(list);
9867
+ } catch (e) {
9868
+ setError(e.message);
9869
+ } finally {
9870
+ setLoading(false);
9871
+ }
9872
+ };
9873
+ useEffect(() => {
9874
+ void loadData();
9875
+ }, []);
9876
+ const review = async (item, action) => {
9877
+ try {
9878
+ await api.reviewSubmission({
9879
+ submissionId: item.id,
9880
+ reviewerId: adminId,
9881
+ action,
9882
+ rejectReason: action === "reject" ? "\u7BA1\u7406\u5458\u9A73\u56DE" : void 0
9883
+ });
9884
+ await loadData();
9885
+ } catch (e) {
9886
+ setError(e.message);
9887
+ }
9888
+ };
9889
+ const toggleVoting = async (enabled) => {
9890
+ if (!snapshot) return;
9891
+ try {
9892
+ await api.updateContestConfig({
9893
+ toggles: {
9894
+ ...snapshot.contest.toggles,
9895
+ votingEnabled: enabled
9896
+ }
9897
+ });
9898
+ await loadData();
9899
+ } catch (e) {
9900
+ setError(e.message);
9901
+ }
9902
+ };
9903
+ const setRestriction = async (banned) => {
9904
+ if (!snapshot || !voterId.trim()) return;
9905
+ try {
9906
+ await api.setVoterRestriction({
9907
+ voterId: voterId.trim(),
9908
+ banned,
9909
+ reason: banned ? "\u7BA1\u7406\u5458\u624B\u52A8\u5C01\u7981" : "\u7BA1\u7406\u5458\u89E3\u9664\u5C01\u7981",
9910
+ operatorId: adminId
9911
+ });
9912
+ setVoterId("");
9913
+ } catch (e) {
9914
+ setError(e.message);
9915
+ }
9916
+ };
9917
+ const resetVotesByVoter = async () => {
9918
+ if (!voterId.trim()) return;
9919
+ try {
9920
+ await api.resetVotes({ voterId: voterId.trim() });
9921
+ setVoterId("");
9922
+ await loadData();
9923
+ } catch (e) {
9924
+ setError(e.message);
9925
+ }
9926
+ };
9927
+ const exportExcel = async () => {
9928
+ try {
9929
+ const data = await api.exportSubmissions();
9930
+ const blob = new Blob([data], {
9931
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
9932
+ });
9933
+ const url = URL.createObjectURL(blob);
9934
+ const a = document.createElement("a");
9935
+ a.href = url;
9936
+ a.download = "miku-submissions.xlsx";
9937
+ a.click();
9938
+ URL.revokeObjectURL(url);
9939
+ } catch (e) {
9940
+ setError(e.message);
9941
+ }
9942
+ };
9943
+ return /* @__PURE__ */ React69__default.createElement("section", null, /* @__PURE__ */ React69__default.createElement("h2", null, title), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => void loadData(), disabled: loading }, loading ? "\u5237\u65B0\u4E2D..." : "\u5237\u65B0\u6570\u636E"), error ? /* @__PURE__ */ React69__default.createElement("p", { style: { color: "crimson" } }, "\u9519\u8BEF\uFF1A", error) : null, /* @__PURE__ */ React69__default.createElement("div", null, /* @__PURE__ */ React69__default.createElement("h3", null, "\u8D5B\u4E8B\u5F00\u5173"), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => void toggleVoting(true), disabled: !snapshot }, "\u5F00\u542F\u6295\u7968"), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => void toggleVoting(false), disabled: !snapshot }, "\u5173\u95ED\u6295\u7968")), /* @__PURE__ */ React69__default.createElement("div", null, /* @__PURE__ */ React69__default.createElement("h3", null, "\u6295\u7A3F\u5BA1\u6838\uFF08", submissions.length, "\uFF09"), /* @__PURE__ */ React69__default.createElement("ul", null, submissions.map((item) => /* @__PURE__ */ React69__default.createElement("li", { key: item.id }, /* @__PURE__ */ React69__default.createElement("strong", null, item.title), "\uFF5C\u4F5C\u8005\uFF1A", item.authorNickname, "\uFF5C\u72B6\u6001\uFF1A", item.status, "\uFF5C\u7968\u6570\uFF1A", item.voteCount, item.status === "pending" ? /* @__PURE__ */ React69__default.createElement(React69__default.Fragment, null, " ", /* @__PURE__ */ React69__default.createElement("button", { onClick: () => void review(item, "approve") }, "\u901A\u8FC7"), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => void review(item, "reject") }, "\u9A73\u56DE")) : null)))), /* @__PURE__ */ React69__default.createElement("div", null, /* @__PURE__ */ React69__default.createElement("h3", null, "\u6295\u7968\u98CE\u63A7"), /* @__PURE__ */ React69__default.createElement("input", { value: voterId, onChange: (e) => setVoterId(e.target.value), placeholder: "voterId" }), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => void setRestriction(true), disabled: !snapshot }, "\u5C01\u7981\u6295\u7968"), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => void setRestriction(false), disabled: !snapshot }, "\u89E3\u9664\u5C01\u7981"), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => void resetVotesByVoter() }, "\u6E05\u96F6\u8BE5\u7528\u6237\u7968\u6570")), /* @__PURE__ */ React69__default.createElement("div", null, /* @__PURE__ */ React69__default.createElement("h3", null, "\u5BFC\u51FA"), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => void exportExcel() }, "\u5BFC\u51FA\u6295\u7A3F Excel")));
9944
+ };
9945
+ var MikuContestAdminPage_default = MikuContestAdminPage;
9946
+
9947
+ // src/mikuContest/ui/web/pages/MikuContestPage.tsx
9948
+ var MikuContestPage = ({
9949
+ defaultView = "audience",
9950
+ viewerVoterId = "viewer-demo",
9951
+ artistId = "artist-demo",
9952
+ artistNickname = "Demo \u753B\u5E08",
9953
+ adminId = "admin-demo"
9954
+ }) => {
9955
+ const [view, setView] = useState(defaultView);
9956
+ return /* @__PURE__ */ React69__default.createElement("div", null, /* @__PURE__ */ React69__default.createElement("h1", null, "Miku Contest"), /* @__PURE__ */ React69__default.createElement("div", null, /* @__PURE__ */ React69__default.createElement("button", { onClick: () => setView("audience") }, "\u89C2\u4F17\u7AEF"), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => setView("artist") }, "\u753B\u5E08\u7AEF"), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => setView("admin") }, "\u7BA1\u7406\u5458\u7AEF")), view === "audience" ? /* @__PURE__ */ React69__default.createElement(MikuContestAudiencePage_default, { voterId: viewerVoterId }) : null, view === "artist" ? /* @__PURE__ */ React69__default.createElement(MikuContestArtistPage_default, { authorId: artistId, authorNickname: artistNickname }) : null, view === "admin" ? /* @__PURE__ */ React69__default.createElement(MikuContestAdminPage_default, { adminId }) : null);
9957
+ };
9958
+ var MikuContestPage_default = MikuContestPage;
9959
+
9960
+ // src/mikuContest/ui/miniapp/index.ts
9961
+ var miniapp_exports2 = {};
9962
+ __export(miniapp_exports2, {
9963
+ MikuContestMiniappHome: () => MikuContestMiniappHome_default,
9964
+ MikuContestMiniappPage: () => MikuContestMiniappPage_default
9965
+ });
9966
+ var MikuContestMiniappHome = ({ snapshot }) => {
9967
+ return /* @__PURE__ */ React69__default.createElement("div", null, /* @__PURE__ */ React69__default.createElement("h3", null, snapshot.contest.name), /* @__PURE__ */ React69__default.createElement("p", null, "\u6295\u7A3F\uFF1A", snapshot.submissions.length, " | \u516C\u544A\uFF1A", snapshot.announcements.length), /* @__PURE__ */ React69__default.createElement("ol", null, snapshot.leaderboard.slice(0, 3).map((item) => /* @__PURE__ */ React69__default.createElement("li", { key: item.submissionId }, item.title, "\uFF08", item.voteCount, "\u7968\uFF09"))));
9968
+ };
9969
+ var MikuContestMiniappHome_default = MikuContestMiniappHome;
9970
+ var MikuContestMiniappPage = () => {
9971
+ const { service, snapshot, refresh } = useMikuContest();
9972
+ const [tab, setTab] = useState("vote");
9973
+ const [voterId, setVoterId] = useState("miniapp-voter");
9974
+ const [authorId, setAuthorId] = useState("miniapp-author");
9975
+ const [authorNickname, setAuthorNickname] = useState("\u5C0F\u7A0B\u5E8F\u753B\u5E08");
9976
+ const [title, setTitle] = useState("");
9977
+ const [desc, setDesc] = useState("");
9978
+ const [type, setType] = useState("visual");
9979
+ const [error, setError] = useState(null);
9980
+ const approvedWorks = useMemo(() => {
9981
+ return snapshot.submissions.filter((item) => item.status === "approved");
9982
+ }, [snapshot.submissions]);
9983
+ const vote = (submissionId) => {
9984
+ try {
9985
+ service.vote({
9986
+ contestId: snapshot.contest.id,
9987
+ submissionId,
9988
+ voterId
9989
+ });
9990
+ refresh();
9991
+ setError(null);
9992
+ } catch (e) {
9993
+ setError(e.message);
9994
+ }
9995
+ };
9996
+ const submit = () => {
9997
+ try {
9998
+ service.createSubmission(
9999
+ {
10000
+ contestId: snapshot.contest.id,
10001
+ authorId,
10002
+ authorNickname,
10003
+ title,
10004
+ description: desc,
10005
+ type,
10006
+ content: {}
10007
+ },
10008
+ "miniapp"
10009
+ );
10010
+ setTitle("");
10011
+ setDesc("");
10012
+ refresh();
10013
+ setError(null);
10014
+ } catch (e) {
10015
+ setError(e.message);
10016
+ }
10017
+ };
10018
+ return /* @__PURE__ */ React69__default.createElement("div", null, /* @__PURE__ */ React69__default.createElement("h3", null, snapshot.contest.name), /* @__PURE__ */ React69__default.createElement("p", null, "\u5C0F\u7A0B\u5E8F\u7AEF\u793A\u4F8B\u9875\u9762"), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => setTab("vote") }, "\u89C2\u4F17\u6295\u7968"), /* @__PURE__ */ React69__default.createElement("button", { onClick: () => setTab("submit") }, "\u753B\u5E08\u6295\u7A3F"), error ? /* @__PURE__ */ React69__default.createElement("p", { style: { color: "crimson" } }, "\u9519\u8BEF\uFF1A", error) : null, tab === "vote" ? /* @__PURE__ */ React69__default.createElement("div", null, /* @__PURE__ */ React69__default.createElement("input", { value: voterId, onChange: (e) => setVoterId(e.target.value), placeholder: "voterId" }), /* @__PURE__ */ React69__default.createElement("ul", null, approvedWorks.map((item) => /* @__PURE__ */ React69__default.createElement("li", { key: item.id }, item.title, "\uFF08", item.voteCount, "\u7968\uFF09", /* @__PURE__ */ React69__default.createElement("button", { onClick: () => vote(item.id) }, "\u6295\u7968"))))) : null, tab === "submit" ? /* @__PURE__ */ React69__default.createElement("div", null, /* @__PURE__ */ React69__default.createElement("input", { value: authorId, onChange: (e) => setAuthorId(e.target.value), placeholder: "authorId" }), /* @__PURE__ */ React69__default.createElement("input", { value: authorNickname, onChange: (e) => setAuthorNickname(e.target.value), placeholder: "\u4F5C\u8005\u6635\u79F0" }), /* @__PURE__ */ React69__default.createElement("input", { value: title, onChange: (e) => setTitle(e.target.value), placeholder: "\u4F5C\u54C1\u6807\u9898" }), /* @__PURE__ */ React69__default.createElement("input", { value: desc, onChange: (e) => setDesc(e.target.value), placeholder: "\u4F5C\u54C1\u7B80\u4ECB" }), /* @__PURE__ */ React69__default.createElement("select", { value: type, onChange: (e) => setType(e.target.value) }, /* @__PURE__ */ React69__default.createElement("option", { value: "visual" }, "visual"), /* @__PURE__ */ React69__default.createElement("option", { value: "video" }, "video"), /* @__PURE__ */ React69__default.createElement("option", { value: "text" }, "text"), /* @__PURE__ */ React69__default.createElement("option", { value: "audio" }, "audio")), /* @__PURE__ */ React69__default.createElement("button", { onClick: submit }, "\u63D0\u4EA4")) : null);
10019
+ };
10020
+ var MikuContestMiniappPage_default = MikuContestMiniappPage;
10021
+
8777
10022
  // src/storage/adapters/react-native-adapter.ts
8778
10023
  var AsyncStorage = null;
8779
10024
  try {
@@ -9127,6 +10372,6 @@ function useElectronStorage(key, defaultValue) {
9127
10372
  return useStorage(electronStorage, key, defaultValue);
9128
10373
  }
9129
10374
 
9130
- export { About_default as About, Dialog as AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, DialogDescription as AlertDialogDescription, DialogFooter as AlertDialogFooter, DialogHeader as AlertDialogHeader, DialogOverlay as AlertDialogOverlay, DialogPortal as AlertDialogPortal, DialogTitle as AlertDialogTitle, DialogTrigger as AlertDialogTrigger, AutoOpenModal, Avatar, AvatarFallback, AvatarImage, BackButton, BackgroundRemover, Badge, BoothConfigPage, BoothRedeemPanel, BoothSuccessCard, BoothUploadPanel, BoothVaultService, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CategoryFilter, CollisionBalls, CompletionFilterComponent, ConfirmModal, ConsoleLoggerAdapter, Contact_default as Contact, DANMAKU_MAX_LENGTH, DANMAKU_TRACK_COUNT, DEFAULT_FESTIVAL_CARD_CONFIG, DEFAULT_MAX_ACTIVE_FIREWORKS, DEFAULT_MAX_PARTICLES, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DanmakuPanel, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DraggableExperimentGrid, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, EnhancedAvatar, ExperimentCard, ExperimentGrid, ExperimentItemGrid, FIREWORK_KIND_LABELS, FestivalCardBook3D, FestivalCardConfigEditor, FestivalCardConfigPage, FestivalCardManagedPage, FestivalCardPageRenderer, FestivalCardService, FestivalCardStudio, FilterButtonGroup, FireworksCanvas, FireworksControlPanel, FloatingMenu_default as FloatingMenu, FloatingMenuExample_default as FloatingMenuExample, GenericOrderManager, Grid, Home_default as Home, ImageMappingPanel, InMemorySkillRegistry, Input, Label, LocalImageMappingPanel, LogLevel, Logger, MIKU_PALETTE, MikuFireworks3D, Modal, NORMAL_PALETTE, Navigation_default as Navigation, NavigationItem_default as NavigationItem, NavigationToggle_default as NavigationToggle, OCRScanner, PageHeader, PermissionGuard, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, ProfileButton, ProfileModal, Progress, ProjectCarousel, ScreenReceiverPanel, ScrollArea, ScrollBar, SearchBox, SearchResultHint, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, SentimentAnalyzer, Separator, Dialog as Sheet, DialogClose as SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, DialogOverlay as SheetOverlay, DialogPortal as SheetPortal, SheetTitle, DialogTrigger as SheetTrigger, SmartAssistant, SortControl, SortModeToggle, SortableExperimentItem, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, Timeline, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UserInfoBar, WebSocketTransport, applyPromptTemplate, arrayUtils, badgeVariants, buttonVariants, cn, createAiClient, createChatSession, createInMemoryFestivalCardDb, createLogger, createOpenAICompatibleProvider, createSkillRegistry, debugUtils, defaultVocaloidBoothConfig, errorUtils, fileUtils, filterExperiments, formatTime, generateMatchCode, getAllTags, getCategoryColor, getCategoryDisplayName, getCompletionFilterDisplayName, getCompletionStatusColor, getCompletionStatusText, getExperimentCounts, japaneseUtils, logger, normalizeFestivalCardConfig, normalizeMatchCode, normalizePromptVariables, normalizeVocaloidBoothConfig, resizeFestivalCardPages, resolveScreenReceiverSignalUrl, skillToToolDefinition, sortExperiments, stringUtils, useAiChat, useAsyncStorage, useBackgroundRemoval, useDanmakuController, useElectronStorage, useFestivalCardConfig, useFireworksEngine, useFireworksRealtime, useLocalStorage, useOCR, useScreenReceiver, useSentimentAnalysis, useStorage, useTaroStorage, useTextGeneration, validateExperiment, validators };
10375
+ export { About_default as About, Dialog as AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, DialogDescription as AlertDialogDescription, DialogFooter as AlertDialogFooter, DialogHeader as AlertDialogHeader, DialogOverlay as AlertDialogOverlay, DialogPortal as AlertDialogPortal, DialogTitle as AlertDialogTitle, DialogTrigger as AlertDialogTrigger, AutoOpenModal, Avatar, AvatarFallback, AvatarImage, BackButton, BackgroundRemover, Badge, BoothConfigPage, BoothRedeemPanel, BoothSuccessCard, BoothUploadPanel, BoothVaultService, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CategoryFilter, CollisionBalls, CompletionFilterComponent, ConfirmModal, ConsoleLoggerAdapter, Contact_default as Contact, DANMAKU_MAX_LENGTH, DANMAKU_TRACK_COUNT, DEFAULT_FESTIVAL_CARD_CONFIG, DEFAULT_MAX_ACTIVE_FIREWORKS, DEFAULT_MAX_PARTICLES, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DanmakuPanel, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DraggableExperimentGrid, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, EnhancedAvatar, ExperimentCard, ExperimentGrid, ExperimentItemGrid, FIREWORK_KIND_LABELS, FestivalCardBook3D, FestivalCardConfigEditor, FestivalCardConfigPage, FestivalCardManagedPage, FestivalCardPageRenderer, FestivalCardService, FestivalCardStudio, FilterButtonGroup, FireworksCanvas, FireworksControlPanel, FloatingMenu_default as FloatingMenu, FloatingMenuExample_default as FloatingMenuExample, GenericOrderManager, Grid, Home_default as Home, ImageMappingPanel, InMemorySkillRegistry, Input, Label, LocalImageMappingPanel, LogLevel, Logger, MIKU_PALETTE, MikuContestPersistentService, MikuContestService, MikuContestStateDbService, MikuFireworks3D, Modal, NORMAL_PALETTE, Navigation_default as Navigation, NavigationItem_default as NavigationItem, NavigationToggle_default as NavigationToggle, OCRScanner, PageHeader, PermissionGuard, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, ProfileButton, ProfileModal, Progress, ProjectCarousel, ScreenReceiverPanel, ScrollArea, ScrollBar, SearchBox, SearchResultHint, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, SentimentAnalyzer, Separator, Dialog as Sheet, DialogClose as SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, DialogOverlay as SheetOverlay, DialogPortal as SheetPortal, SheetTitle, DialogTrigger as SheetTrigger, SmartAssistant, SortControl, SortModeToggle, SortableExperimentItem, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, Timeline, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UserInfoBar, WebSocketTransport, applyPromptTemplate, arrayUtils, badgeVariants, buttonVariants, checkVoteEligibility, cn, createAiClient, createChatSession, createCreateSubmissionHandler, createDefaultMikuContestConfig, createExportSubmissionsHandler, createGetContestSnapshotHandler, createInMemoryFestivalCardDb, createListSubmissionsHandler, createLogger, createMikuContestApiClient, createMikuContestDrizzlePersistenceAdapter, createMikuContestPersistentService, createMikuContestService, createOpenAICompatibleProvider, createResetVotesHandler, createReviewSubmissionHandler, createSetVoterRestrictionHandler, createSkillRegistry, createUpdateContestConfigHandler, createVoteHandler, debugUtils, defaultMikuVotingRules, defaultVocaloidBoothConfig, errorUtils, fileUtils, filterExperiments, formatTime, generateMatchCode, getAllTags, getCategoryColor, getCategoryDisplayName, getCompletionFilterDisplayName, getCompletionStatusColor, getCompletionStatusText, getExperimentCounts, japaneseUtils, logger, mikuContestConfigs, mikuContestDbService, mikuContestNotices, mikuContestSubmissions, mikuContestVoterRestrictions, mikuContestVotes, miniapp_exports as miniappService, miniapp_exports2 as miniappUI, normalizeFestivalCardConfig, normalizeMatchCode, normalizePromptVariables, normalizeVocaloidBoothConfig, resizeFestivalCardPages, resolveScreenReceiverSignalUrl, skillToToolDefinition, sortByVotesDesc, sortExperiments, stringUtils, toVoteDayKey, useAiChat, useAsyncStorage, useBackgroundRemoval, useDanmakuController, useElectronStorage, useFestivalCardConfig, useFireworksEngine, useFireworksRealtime, useLocalStorage, useMikuContest, useOCR, useScreenReceiver, useSentimentAnalysis, useStorage, useTaroStorage, useTextGeneration, validateExperiment, validateMikuSubmissionInput, validators, web_exports as webService, web_exports2 as webUI };
9131
10376
  //# sourceMappingURL=index.mjs.map
9132
10377
  //# sourceMappingURL=index.mjs.map