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.js CHANGED
@@ -18,6 +18,8 @@ var crypto = require('crypto');
18
18
  require('bcryptjs');
19
19
  require('jsonwebtoken');
20
20
  var THREE2 = require('three');
21
+ var XLSX = require('xlsx');
22
+ var server = require('next/server');
21
23
 
22
24
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
23
25
 
@@ -42,13 +44,19 @@ function _interopNamespace(e) {
42
44
  var React69__namespace = /*#__PURE__*/_interopNamespace(React69);
43
45
  var Link__default = /*#__PURE__*/_interopDefault(Link);
44
46
  var THREE2__namespace = /*#__PURE__*/_interopNamespace(THREE2);
47
+ var XLSX__namespace = /*#__PURE__*/_interopNamespace(XLSX);
45
48
 
49
+ var __defProp = Object.defineProperty;
46
50
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
47
51
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
48
52
  }) : x)(function(x) {
49
53
  if (typeof require !== "undefined") return require.apply(this, arguments);
50
54
  throw Error('Dynamic require of "' + x + '" is not supported');
51
55
  });
56
+ var __export = (target, all) => {
57
+ for (var name in all)
58
+ __defProp(target, name, { get: all[name], enumerable: true });
59
+ };
52
60
 
53
61
  // src/logger/types.ts
54
62
  var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
@@ -76,10 +84,10 @@ var ConsoleLoggerAdapter = class {
76
84
  };
77
85
  }
78
86
  log(entry) {
79
- const { level, message, timestamp: timestamp5, data, context, error } = entry;
87
+ const { level, message, timestamp: timestamp6, data, context, error } = entry;
80
88
  let logMessage = "";
81
- if (timestamp5) {
82
- logMessage += "[" + this.formatTimestamp(timestamp5) + "] ";
89
+ if (timestamp6) {
90
+ logMessage += "[" + this.formatTimestamp(timestamp6) + "] ";
83
91
  }
84
92
  const levelName = this.getLevelName(level);
85
93
  logMessage += levelName + ": ";
@@ -572,7 +580,7 @@ var useSentimentAnalysis = (options = {}) => {
572
580
  result: null
573
581
  });
574
582
  const pipelineRef = React69.useRef(null);
575
- const analyze = React69.useCallback(async (text5) => {
583
+ const analyze = React69.useCallback(async (text6) => {
576
584
  setState((prev) => ({
577
585
  ...prev,
578
586
  isProcessing: true,
@@ -619,12 +627,12 @@ var useSentimentAnalysis = (options = {}) => {
619
627
  pipelineRef.current = await pipeline("sentiment-analysis", options.model || defaultModel);
620
628
  }
621
629
  setState((prev) => ({ ...prev, status: "analyzing" }));
622
- const output = await pipelineRef.current(text5);
630
+ const output = await pipelineRef.current(text6);
623
631
  const resultData = output[0];
624
632
  const label = resultData.label.toLowerCase();
625
633
  let sentiment = "neutral";
626
634
  const negativeKeywords = ["\u7D2F", "\u60E8", "\u7EDD\u671B", "\u96BE\u53D7", "\u4F24\u5FC3", "\u5DEE", "\u574F", "\u7CDF", "\u4E0D\u884C"];
627
- const hasNegativeKeyword = negativeKeywords.some((k) => text5.includes(k));
635
+ const hasNegativeKeyword = negativeKeywords.some((k) => text6.includes(k));
628
636
  if (label.includes("positive") && !hasNegativeKeyword) {
629
637
  sentiment = "positive";
630
638
  } else if (label.includes("negative") || label.includes("0") || hasNegativeKeyword) {
@@ -656,12 +664,12 @@ var SentimentAnalyzer = ({
656
664
  className = "",
657
665
  placeholder = "\u8F93\u5165\u4E00\u6BB5\u4E2D\u6587\u6216\u82F1\u6587\uFF0C\u5206\u6790\u5176\u60C5\u611F\u503E\u5411..."
658
666
  }) => {
659
- const [text5, setText] = React69.useState("");
667
+ const [text6, setText] = React69.useState("");
660
668
  const { analyze, isProcessing, status, result, error } = useSentimentAnalysis();
661
669
  const handleAnalyze = async () => {
662
- if (!text5.trim() || isProcessing) return;
670
+ if (!text6.trim() || isProcessing) return;
663
671
  try {
664
- const res = await analyze(text5);
672
+ const res = await analyze(text6);
665
673
  onResult?.(res);
666
674
  } catch (err) {
667
675
  console.error("Sentiment Analysis Error:", err);
@@ -692,7 +700,7 @@ var SentimentAnalyzer = ({
692
700
  return /* @__PURE__ */ React69__namespace.default.createElement("div", { className: clsx.clsx("p-6 border rounded-xl bg-white dark:bg-gray-800 shadow-sm", className) }, /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "flex items-center gap-2 mb-4 text-gray-700 dark:text-gray-300 font-medium" }, /* @__PURE__ */ React69__namespace.default.createElement(lucideReact.MessageSquare, { size: 20 }), /* @__PURE__ */ React69__namespace.default.createElement("span", null, "\u6587\u672C\u60C5\u611F\u5206\u6790")), /* @__PURE__ */ React69__namespace.default.createElement("div", { className: "relative" }, /* @__PURE__ */ React69__namespace.default.createElement(
693
701
  "textarea",
694
702
  {
695
- value: text5,
703
+ value: text6,
696
704
  onChange: (e) => setText(e.target.value),
697
705
  placeholder,
698
706
  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",
@@ -702,7 +710,7 @@ var SentimentAnalyzer = ({
702
710
  "button",
703
711
  {
704
712
  onClick: handleAnalyze,
705
- disabled: !text5.trim() || isProcessing,
713
+ disabled: !text6.trim() || isProcessing,
706
714
  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"
707
715
  },
708
716
  isProcessing ? /* @__PURE__ */ React69__namespace.default.createElement(lucideReact.Loader2, { className: "animate-spin", size: 18 }) : /* @__PURE__ */ React69__namespace.default.createElement(lucideReact.Send, { size: 18 })
@@ -890,13 +898,13 @@ var requestJson = async (options) => {
890
898
  body: body ? JSON.stringify(body) : void 0,
891
899
  signal: controller?.signal
892
900
  });
893
- const text5 = await response.text();
901
+ const text6 = await response.text();
894
902
  let data = null;
895
- if (text5) {
903
+ if (text6) {
896
904
  try {
897
- data = JSON.parse(text5);
905
+ data = JSON.parse(text6);
898
906
  } catch {
899
- data = text5;
907
+ data = text6;
900
908
  }
901
909
  }
902
910
  if (!response.ok) {
@@ -1357,20 +1365,20 @@ var japaneseUtils = {
1357
1365
  /**
1358
1366
  * 提取文本中的汉字
1359
1367
  */
1360
- extractKanji(text5) {
1361
- return text5.match(/[\u4E00-\u9FAF]/g) || [];
1368
+ extractKanji(text6) {
1369
+ return text6.match(/[\u4E00-\u9FAF]/g) || [];
1362
1370
  },
1363
1371
  /**
1364
1372
  * 提取文本中的假名
1365
1373
  */
1366
- extractKana(text5) {
1367
- return text5.match(/[\u3040-\u309F\u30A0-\u30FF]/g) || [];
1374
+ extractKana(text6) {
1375
+ return text6.match(/[\u3040-\u309F\u30A0-\u30FF]/g) || [];
1368
1376
  },
1369
1377
  /**
1370
1378
  * 清理文本,移除特殊字符但保留日语字符
1371
1379
  */
1372
- cleanText(text5) {
1373
- return text5.replace(/[^\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\w\s]/g, "");
1380
+ cleanText(text6) {
1381
+ return text6.replace(/[^\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\w\s]/g, "");
1374
1382
  }
1375
1383
  };
1376
1384
 
@@ -1459,11 +1467,11 @@ var fileUtils = {
1459
1467
  * 生成唯一文件名
1460
1468
  */
1461
1469
  generateUniqueFileName(originalName) {
1462
- const timestamp5 = Date.now();
1470
+ const timestamp6 = Date.now();
1463
1471
  const random = Math.random().toString(36).substring(2, 15);
1464
1472
  const extension = this.getFileExtension(originalName);
1465
1473
  const baseName = originalName.replace("." + extension, "");
1466
- return extension ? baseName + "_" + timestamp5 + "_" + random + "." + extension : baseName + "_" + timestamp5 + "_" + random;
1474
+ return extension ? baseName + "_" + timestamp6 + "_" + random + "." + extension : baseName + "_" + timestamp6 + "_" + random;
1467
1475
  },
1468
1476
  /**
1469
1477
  * 验证文件名是否有效
@@ -1533,28 +1541,28 @@ var stringUtils = {
1533
1541
  /**
1534
1542
  * 截断文本
1535
1543
  */
1536
- truncate(text5, length, suffix = "...") {
1537
- if (text5.length <= length) return text5;
1538
- return text5.substring(0, length - suffix.length) + suffix;
1544
+ truncate(text6, length, suffix = "...") {
1545
+ if (text6.length <= length) return text6;
1546
+ return text6.substring(0, length - suffix.length) + suffix;
1539
1547
  },
1540
1548
  /**
1541
1549
  * 首字母大写
1542
1550
  */
1543
- capitalize(text5) {
1544
- if (!text5) return "";
1545
- return text5.charAt(0).toUpperCase() + text5.slice(1).toLowerCase();
1551
+ capitalize(text6) {
1552
+ if (!text6) return "";
1553
+ return text6.charAt(0).toUpperCase() + text6.slice(1).toLowerCase();
1546
1554
  },
1547
1555
  /**
1548
1556
  * 驼峰转下划线
1549
1557
  */
1550
- camelToSnake(text5) {
1551
- return text5.replace(/[A-Z]/g, (letter) => "_" + letter.toLowerCase());
1558
+ camelToSnake(text6) {
1559
+ return text6.replace(/[A-Z]/g, (letter) => "_" + letter.toLowerCase());
1552
1560
  },
1553
1561
  /**
1554
1562
  * 下划线转驼峰
1555
1563
  */
1556
- snakeToCamel(text5) {
1557
- return text5.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
1564
+ snakeToCamel(text6) {
1565
+ return text6.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
1558
1566
  },
1559
1567
  /**
1560
1568
  * 生成随机字符串
@@ -5673,9 +5681,9 @@ var UserInfoBar = ({ apiClient }) => {
5673
5681
  ))));
5674
5682
  };
5675
5683
  function DanmakuPanel({ onSend }) {
5676
- const [text5, setText] = React69.useState("");
5684
+ const [text6, setText] = React69.useState("");
5677
5685
  const emit = () => {
5678
- const value = text5.trim();
5686
+ const value = text6.trim();
5679
5687
  if (!value) {
5680
5688
  return;
5681
5689
  }
@@ -5686,7 +5694,7 @@ function DanmakuPanel({ onSend }) {
5686
5694
  "input",
5687
5695
  {
5688
5696
  type: "text",
5689
- value: text5,
5697
+ value: text6,
5690
5698
  onChange: (event) => setText(event.target.value),
5691
5699
  onKeyDown: (event) => {
5692
5700
  if (event.key === "Enter") {
@@ -5802,8 +5810,8 @@ function useDanmakuController(options) {
5802
5810
  cursorRef.current += 1;
5803
5811
  }, []);
5804
5812
  const send = React69.useCallback(
5805
- (text5, color, sendOptions) => {
5806
- const trimmed = text5.trim();
5813
+ (text6, color, sendOptions) => {
5814
+ const trimmed = text6.trim();
5807
5815
  if (!trimmed) {
5808
5816
  return null;
5809
5817
  }
@@ -5840,26 +5848,26 @@ function useDanmakuController(options) {
5840
5848
  [addIncoming, items, removeItem, send]
5841
5849
  );
5842
5850
  }
5843
- function parseCommand(text5) {
5844
- if (text5.startsWith("/miku ")) {
5845
- return { launchKind: "miku", content: text5.replace("/miku ", "").trim() };
5851
+ function parseCommand(text6) {
5852
+ if (text6.startsWith("/miku ")) {
5853
+ return { launchKind: "miku", content: text6.replace("/miku ", "").trim() };
5846
5854
  }
5847
- if (text5 === "/miku") {
5855
+ if (text6 === "/miku") {
5848
5856
  return { launchKind: "miku", content: "MIKU!" };
5849
5857
  }
5850
- if (text5.startsWith("/avatar ")) {
5851
- return { launchKind: "avatar", content: text5.replace("/avatar ", "").trim() };
5858
+ if (text6.startsWith("/avatar ")) {
5859
+ return { launchKind: "avatar", content: text6.replace("/avatar ", "").trim() };
5852
5860
  }
5853
- if (text5 === "/avatar") {
5861
+ if (text6 === "/avatar") {
5854
5862
  return { launchKind: "avatar", content: "Avatar Firework!" };
5855
5863
  }
5856
- if (text5.startsWith("/normal ")) {
5857
- return { launchKind: "normal", content: text5.replace("/normal ", "").trim() };
5864
+ if (text6.startsWith("/normal ")) {
5865
+ return { launchKind: "normal", content: text6.replace("/normal ", "").trim() };
5858
5866
  }
5859
- if (text5 === "/normal") {
5867
+ if (text6 === "/normal") {
5860
5868
  return { launchKind: "normal", content: "Fireworks!" };
5861
5869
  }
5862
- return { content: text5 };
5870
+ return { content: text6 };
5863
5871
  }
5864
5872
  function createCircularSpriteTexture() {
5865
5873
  const size = 64;
@@ -6541,12 +6549,12 @@ var WebSocketTransport = class {
6541
6549
  }
6542
6550
  };
6543
6551
  function parseServerMessage(raw) {
6544
- const text5 = decodeMessage(raw);
6545
- if (!text5) {
6552
+ const text6 = decodeMessage(raw);
6553
+ if (!text6) {
6546
6554
  return null;
6547
6555
  }
6548
6556
  try {
6549
- return JSON.parse(text5);
6557
+ return JSON.parse(text6);
6550
6558
  } catch {
6551
6559
  return null;
6552
6560
  }
@@ -6813,8 +6821,8 @@ function MikuFireworks3D({
6813
6821
  }
6814
6822
  launch(payload);
6815
6823
  };
6816
- const handleSendDanmaku = (text5) => {
6817
- const result = send(text5, void 0, {
6824
+ const handleSendDanmaku = (text6) => {
6825
+ const result = send(text6, void 0, {
6818
6826
  optimistic: !realtimeEnabled
6819
6827
  });
6820
6828
  if (!result) {
@@ -6913,9 +6921,9 @@ function useScreenReceiver(options) {
6913
6921
  const peerRef = React69.useRef({ pendingCandidates: [] });
6914
6922
  const videoRef = React69.useRef(null);
6915
6923
  const appendLog = React69.useCallback(
6916
- (text5) => {
6924
+ (text6) => {
6917
6925
  logIdRef.current += 1;
6918
- setLogs((prev) => [...prev, { id: logIdRef.current, text: text5 }].slice(-maxLogs));
6926
+ setLogs((prev) => [...prev, { id: logIdRef.current, text: text6 }].slice(-maxLogs));
6919
6927
  },
6920
6928
  [maxLogs]
6921
6929
  );
@@ -7641,8 +7649,8 @@ var withRoundedClip = (ctx, left, top, width, height, radius, draw) => {
7641
7649
  draw();
7642
7650
  ctx.restore();
7643
7651
  };
7644
- var drawMultilineText = (ctx, text5, left, top, maxWidth, lineHeight) => {
7645
- const paragraphs = text5.split("\n");
7652
+ var drawMultilineText = (ctx, text6, left, top, maxWidth, lineHeight) => {
7653
+ const paragraphs = text6.split("\n");
7646
7654
  let currentY = top;
7647
7655
  paragraphs.forEach((paragraph, index) => {
7648
7656
  const words = paragraph.split("");
@@ -8799,6 +8807,1244 @@ var BoothConfigPage = ({ initialConfig, onSave }) => {
8799
8807
  )), /* @__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")));
8800
8808
  };
8801
8809
 
8810
+ // src/mikuContest/logic/shared/defaults.ts
8811
+ var defaultMikuVotingRules = {
8812
+ maxVotesPerDay: 3,
8813
+ forbidDuplicateVotePerWork: true,
8814
+ maxVotesPerDevicePerDay: 20,
8815
+ maxVotesPerIpPerDay: 100
8816
+ };
8817
+ var createDefaultMikuContestConfig = (overrides) => ({
8818
+ id: overrides?.id || "miku-contest-default",
8819
+ name: overrides?.name || "\u521D\u97F3\u672A\u6765\u793E\u56E2\u5F81\u7A3F\u5927\u8D5B",
8820
+ theme: overrides?.theme || "\u521D\u97F3\u672A\u6765\u4E3B\u9898\u521B\u4F5C\u5F81\u7A3F",
8821
+ organizer: overrides?.organizer || "\u521D\u97F3\u672A\u6765\u793E\u56E2",
8822
+ awards: overrides?.awards || ["\u4E00\u7B49\u5956", "\u4E8C\u7B49\u5956", "\u4E09\u7B49\u5956", "\u4EBA\u6C14\u5956"],
8823
+ rules: overrides?.rules || "\u8BF7\u786E\u4FDD\u4F5C\u54C1\u539F\u521B\u4E14\u7B26\u5408\u793E\u56E2\u89C4\u8303\u3002",
8824
+ copyright: overrides?.copyright || "\u6295\u7A3F\u5373\u89C6\u4E3A\u6388\u6743\u8D5B\u4E8B\u5C55\u793A\u4E0E\u516C\u793A\u3002",
8825
+ timeline: overrides?.timeline || {
8826
+ submissionStartAt: (/* @__PURE__ */ new Date()).toISOString(),
8827
+ submissionEndAt: (/* @__PURE__ */ new Date()).toISOString(),
8828
+ votingStartAt: (/* @__PURE__ */ new Date()).toISOString(),
8829
+ votingEndAt: (/* @__PURE__ */ new Date()).toISOString(),
8830
+ publicResultAt: (/* @__PURE__ */ new Date()).toISOString()
8831
+ },
8832
+ votingRules: {
8833
+ ...defaultMikuVotingRules,
8834
+ ...overrides?.votingRules || {}
8835
+ },
8836
+ toggles: {
8837
+ submissionEnabled: overrides?.toggles?.submissionEnabled ?? true,
8838
+ votingEnabled: overrides?.toggles?.votingEnabled ?? true,
8839
+ resultEnabled: overrides?.toggles?.resultEnabled ?? false
8840
+ }
8841
+ });
8842
+
8843
+ // src/mikuContest/logic/shared/validators.ts
8844
+ var DESCRIPTION_LIMIT = 500;
8845
+ var MINIAPP_DESCRIPTION_LIMIT = 200;
8846
+ var TEXT_CONTENT_LIMIT = 2e3;
8847
+ var MAX_TAGS = 3;
8848
+ var hasValue = (value) => {
8849
+ return typeof value === "string" ? value.trim().length > 0 : value !== null && value !== void 0;
8850
+ };
8851
+ var validateByType = (type, input) => {
8852
+ const errors = [];
8853
+ const { content } = input;
8854
+ switch (type) {
8855
+ case "visual": {
8856
+ const imageCount = content.images?.length || 0;
8857
+ if (imageCount < 1 || imageCount > 3) {
8858
+ errors.push("\u89C6\u89C9\u7C7B\u4F5C\u54C1\u9700\u4E0A\u4F20 1-3 \u5F20\u56FE\u7247");
8859
+ }
8860
+ break;
8861
+ }
8862
+ case "video": {
8863
+ if (!hasValue(content.videoLink)) {
8864
+ errors.push("\u89C6\u9891\u7C7B\u4F5C\u54C1\u9700\u63D0\u4F9B\u89C6\u9891\u94FE\u63A5");
8865
+ }
8866
+ if (!hasValue(content.coverImage)) {
8867
+ errors.push("\u89C6\u9891\u7C7B\u4F5C\u54C1\u9700\u63D0\u4F9B\u5C01\u9762\u56FE");
8868
+ }
8869
+ break;
8870
+ }
8871
+ case "text": {
8872
+ const text6 = content.textContent || "";
8873
+ if (!text6.trim()) {
8874
+ errors.push("\u6587\u5B57\u7C7B\u4F5C\u54C1\u9700\u586B\u5199\u6B63\u6587");
8875
+ }
8876
+ if (text6.length > TEXT_CONTENT_LIMIT) {
8877
+ errors.push(`\u6587\u5B57\u6B63\u6587\u4E0D\u80FD\u8D85\u8FC7 ${TEXT_CONTENT_LIMIT} \u5B57`);
8878
+ }
8879
+ break;
8880
+ }
8881
+ case "audio": {
8882
+ if (!hasValue(content.audioLink)) {
8883
+ errors.push("\u97F3\u9891\u7C7B\u4F5C\u54C1\u9700\u63D0\u4F9B\u97F3\u9891\u94FE\u63A5");
8884
+ }
8885
+ break;
8886
+ }
8887
+ }
8888
+ return errors;
8889
+ };
8890
+ var validateMikuSubmissionInput = (input, mode = "web") => {
8891
+ const errors = [];
8892
+ if (!input.contestId.trim()) errors.push("contestId \u4E0D\u80FD\u4E3A\u7A7A");
8893
+ if (!input.authorId.trim()) errors.push("authorId \u4E0D\u80FD\u4E3A\u7A7A");
8894
+ if (!input.authorNickname.trim()) errors.push("\u4F5C\u8005\u6635\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
8895
+ if (!input.title.trim()) errors.push("\u4F5C\u54C1\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
8896
+ const descriptionLimit = mode === "miniapp" ? MINIAPP_DESCRIPTION_LIMIT : DESCRIPTION_LIMIT;
8897
+ if (input.description.length > descriptionLimit) {
8898
+ errors.push(`\u4F5C\u54C1\u7B80\u4ECB\u4E0D\u80FD\u8D85\u8FC7 ${descriptionLimit} \u5B57`);
8899
+ }
8900
+ if ((input.tags?.length || 0) > MAX_TAGS) {
8901
+ errors.push(`\u6807\u7B7E\u6700\u591A ${MAX_TAGS} \u4E2A`);
8902
+ }
8903
+ errors.push(...validateByType(input.type, input));
8904
+ return errors;
8905
+ };
8906
+
8907
+ // src/mikuContest/logic/shared/voting.ts
8908
+ var toVoteDayKey = (date = /* @__PURE__ */ new Date()) => {
8909
+ const y = date.getUTCFullYear();
8910
+ const m = String(date.getUTCMonth() + 1).padStart(2, "0");
8911
+ const d = String(date.getUTCDate()).padStart(2, "0");
8912
+ return `${y}-${m}-${d}`;
8913
+ };
8914
+ var checkVoteEligibility = (context) => {
8915
+ const { existingVotes, submissionId, voterId, dayKey, rules } = context;
8916
+ const userTodayVotes = existingVotes.filter((vote) => vote.voterId === voterId && vote.dayKey === dayKey);
8917
+ if (userTodayVotes.length >= rules.maxVotesPerDay) {
8918
+ return { ok: false, reason: "\u5DF2\u8FBE\u5230\u4ECA\u65E5\u6295\u7968\u4E0A\u9650" };
8919
+ }
8920
+ if (rules.forbidDuplicateVotePerWork) {
8921
+ const duplicated = userTodayVotes.some((vote) => vote.submissionId === submissionId);
8922
+ if (duplicated) return { ok: false, reason: "\u4E0D\u53EF\u91CD\u590D\u6295\u540C\u4E00\u4F5C\u54C1" };
8923
+ }
8924
+ return { ok: true };
8925
+ };
8926
+ var sortByVotesDesc = (items) => {
8927
+ return [...items].sort((a, b) => {
8928
+ if (b.voteCount === a.voteCount) {
8929
+ return (a.createdAt || "").localeCompare(b.createdAt || "");
8930
+ }
8931
+ return b.voteCount - a.voteCount;
8932
+ });
8933
+ };
8934
+ var randomId = (prefix) => {
8935
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
8936
+ };
8937
+ var serialNo = () => {
8938
+ const now = /* @__PURE__ */ new Date();
8939
+ const y = now.getFullYear();
8940
+ const m = String(now.getMonth() + 1).padStart(2, "0");
8941
+ const d = String(now.getDate()).padStart(2, "0");
8942
+ const seq = Math.floor(Math.random() * 9e3 + 1e3);
8943
+ return `MIKU-${y}${m}${d}-${seq}`;
8944
+ };
8945
+ var MikuContestService = class {
8946
+ constructor(options = {}) {
8947
+ this.submissions = /* @__PURE__ */ new Map();
8948
+ this.votes = [];
8949
+ this.announcements = /* @__PURE__ */ new Map();
8950
+ this.voterRestrictions = /* @__PURE__ */ new Map();
8951
+ this.contest = createDefaultMikuContestConfig(options.contestConfig);
8952
+ }
8953
+ getContestConfig() {
8954
+ return this.contest;
8955
+ }
8956
+ updateContestConfig(patch) {
8957
+ this.contest = {
8958
+ ...this.contest,
8959
+ ...patch,
8960
+ votingRules: {
8961
+ ...this.contest.votingRules,
8962
+ ...patch.votingRules || {}
8963
+ },
8964
+ toggles: {
8965
+ ...this.contest.toggles,
8966
+ ...patch.toggles || {}
8967
+ },
8968
+ timeline: {
8969
+ ...this.contest.timeline,
8970
+ ...patch.timeline || {}
8971
+ }
8972
+ };
8973
+ return this.contest;
8974
+ }
8975
+ createSubmission(input, mode = "web") {
8976
+ if (!this.contest.toggles.submissionEnabled) {
8977
+ throw new Error("\u5F53\u524D\u672A\u5F00\u653E\u6295\u7A3F");
8978
+ }
8979
+ const errors = validateMikuSubmissionInput(input, mode);
8980
+ if (errors.length > 0) {
8981
+ throw new Error(errors.join("\uFF1B"));
8982
+ }
8983
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8984
+ const next = {
8985
+ id: randomId("submission"),
8986
+ serialNo: serialNo(),
8987
+ contestId: input.contestId,
8988
+ authorId: input.authorId,
8989
+ authorNickname: input.authorNickname,
8990
+ title: input.title,
8991
+ type: input.type,
8992
+ description: input.description,
8993
+ tags: input.tags || [],
8994
+ content: input.content,
8995
+ voteCount: 0,
8996
+ status: "pending",
8997
+ createdAt: now,
8998
+ updatedAt: now
8999
+ };
9000
+ this.submissions.set(next.id, next);
9001
+ return next;
9002
+ }
9003
+ listSubmissions(filter) {
9004
+ const authorKeyword = filter?.authorKeyword?.trim().toLowerCase();
9005
+ const titleKeyword = filter?.titleKeyword?.trim().toLowerCase();
9006
+ return [...this.submissions.values()].filter((item) => {
9007
+ if (filter?.status && item.status !== filter.status) return false;
9008
+ if (filter?.type && item.type !== filter.type) return false;
9009
+ if (filter?.authorId && item.authorId !== filter.authorId) return false;
9010
+ if (authorKeyword && !item.authorNickname.toLowerCase().includes(authorKeyword)) return false;
9011
+ if (titleKeyword && !item.title.toLowerCase().includes(titleKeyword)) return false;
9012
+ return true;
9013
+ });
9014
+ }
9015
+ getSubmission(submissionId) {
9016
+ return this.submissions.get(submissionId) || null;
9017
+ }
9018
+ reviewSubmission(input) {
9019
+ const current = this.submissions.get(input.submissionId);
9020
+ if (!current) throw new Error("\u6295\u7A3F\u4E0D\u5B58\u5728");
9021
+ if (input.action === "reject" && !input.rejectReason?.trim()) {
9022
+ throw new Error("\u9A73\u56DE\u9700\u586B\u5199\u539F\u56E0");
9023
+ }
9024
+ const reviewed = {
9025
+ ...current,
9026
+ status: input.action === "approve" ? "approved" : "rejected",
9027
+ rejectReason: input.action === "reject" ? input.rejectReason?.trim() : void 0,
9028
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
9029
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9030
+ };
9031
+ this.submissions.set(reviewed.id, reviewed);
9032
+ return reviewed;
9033
+ }
9034
+ vote(input) {
9035
+ if (!this.contest.toggles.votingEnabled) {
9036
+ throw new Error("\u5F53\u524D\u672A\u5F00\u653E\u6295\u7968");
9037
+ }
9038
+ const restriction = this.voterRestrictions.get(input.voterId);
9039
+ if (restriction?.banned) {
9040
+ throw new Error("\u5F53\u524D\u8D26\u53F7\u5DF2\u88AB\u9650\u5236\u6295\u7968");
9041
+ }
9042
+ const target = this.submissions.get(input.submissionId);
9043
+ if (!target) throw new Error("\u4F5C\u54C1\u4E0D\u5B58\u5728");
9044
+ if (target.status !== "approved") throw new Error("\u4EC5\u53EF\u5BF9\u5DF2\u8FC7\u5BA1\u4F5C\u54C1\u6295\u7968");
9045
+ const dayKey = toVoteDayKey();
9046
+ const eligible = checkVoteEligibility({
9047
+ existingVotes: this.votes,
9048
+ submissionId: input.submissionId,
9049
+ voterId: input.voterId,
9050
+ dayKey,
9051
+ rules: this.contest.votingRules
9052
+ });
9053
+ if (!eligible.ok) {
9054
+ throw new Error(eligible.reason || "\u6295\u7968\u5931\u8D25");
9055
+ }
9056
+ const vote = {
9057
+ id: randomId("vote"),
9058
+ contestId: input.contestId,
9059
+ submissionId: input.submissionId,
9060
+ voterId: input.voterId,
9061
+ votedAt: (/* @__PURE__ */ new Date()).toISOString(),
9062
+ dayKey,
9063
+ deviceId: input.deviceId,
9064
+ ip: input.ip
9065
+ };
9066
+ this.votes.push(vote);
9067
+ const updated = {
9068
+ ...target,
9069
+ voteCount: target.voteCount + 1,
9070
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9071
+ };
9072
+ this.submissions.set(updated.id, updated);
9073
+ return updated;
9074
+ }
9075
+ getVoterRestriction(voterId) {
9076
+ return this.voterRestrictions.get(voterId) || null;
9077
+ }
9078
+ setVoterRestriction(input) {
9079
+ const next = {
9080
+ voterId: input.voterId,
9081
+ banned: input.banned,
9082
+ reason: input.reason,
9083
+ operatorId: input.operatorId,
9084
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9085
+ };
9086
+ this.voterRestrictions.set(input.voterId, next);
9087
+ return next;
9088
+ }
9089
+ resetVotes(input) {
9090
+ if (!input.submissionId && !input.voterId) {
9091
+ throw new Error("submissionId \u4E0E voterId \u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A");
9092
+ }
9093
+ const before = this.votes.length;
9094
+ const affected = /* @__PURE__ */ new Set();
9095
+ const remained = this.votes.filter((vote) => {
9096
+ const matchSubmission = input.submissionId ? vote.submissionId === input.submissionId : true;
9097
+ const matchVoter = input.voterId ? vote.voterId === input.voterId : true;
9098
+ const shouldRemove = matchSubmission && matchVoter;
9099
+ if (shouldRemove) affected.add(vote.submissionId);
9100
+ return !shouldRemove;
9101
+ });
9102
+ this.votes.length = 0;
9103
+ this.votes.push(...remained);
9104
+ this.recalculateVoteCounts();
9105
+ return {
9106
+ removedVotes: before - remained.length,
9107
+ affectedSubmissions: [...affected]
9108
+ };
9109
+ }
9110
+ listAnnouncements(contestId) {
9111
+ const all = [...this.announcements.values()];
9112
+ return contestId ? all.filter((item) => item.contestId === contestId) : all;
9113
+ }
9114
+ publishAnnouncement(input) {
9115
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9116
+ const announcement = {
9117
+ id: randomId("notice"),
9118
+ contestId: input.contestId,
9119
+ title: input.title,
9120
+ content: input.content,
9121
+ type: input.type,
9122
+ createdBy: input.createdBy,
9123
+ createdAt: now,
9124
+ updatedAt: now
9125
+ };
9126
+ this.announcements.set(announcement.id, announcement);
9127
+ return announcement;
9128
+ }
9129
+ getLeaderboard(limit = 10) {
9130
+ const ranked = sortByVotesDesc(this.listSubmissions({ status: "approved" })).slice(0, limit);
9131
+ return ranked.map((item, index) => ({
9132
+ submissionId: item.id,
9133
+ title: item.title,
9134
+ authorNickname: item.authorNickname,
9135
+ voteCount: item.voteCount,
9136
+ rank: index + 1
9137
+ }));
9138
+ }
9139
+ getSnapshot() {
9140
+ return {
9141
+ contest: this.contest,
9142
+ submissions: this.listSubmissions(),
9143
+ announcements: this.listAnnouncements(),
9144
+ leaderboard: this.getLeaderboard()
9145
+ };
9146
+ }
9147
+ getSubmissionExportRows(filter) {
9148
+ return this.listSubmissions(filter).map((item) => ({
9149
+ \u6295\u7A3F\u7F16\u53F7: item.serialNo,
9150
+ \u6295\u7A3FID: item.id,
9151
+ \u8D5B\u4E8BID: item.contestId,
9152
+ \u4F5C\u8005ID: item.authorId,
9153
+ \u4F5C\u8005\u6635\u79F0: item.authorNickname,
9154
+ \u4F5C\u54C1\u540D\u79F0: item.title,
9155
+ \u4F5C\u54C1\u7C7B\u578B: item.type,
9156
+ \u7B80\u4ECB: item.description,
9157
+ \u6807\u7B7E: item.tags.join(","),
9158
+ \u5BA1\u6838\u72B6\u6001: item.status,
9159
+ \u9A73\u56DE\u539F\u56E0: item.rejectReason || "",
9160
+ \u7968\u6570: item.voteCount,
9161
+ \u63D0\u4EA4\u65F6\u95F4: item.createdAt,
9162
+ \u66F4\u65B0\u65F6\u95F4: item.updatedAt
9163
+ }));
9164
+ }
9165
+ exportSubmissionExcel(filter) {
9166
+ const rows = this.getSubmissionExportRows(filter);
9167
+ const workbook = XLSX__namespace.utils.book_new();
9168
+ const worksheet = XLSX__namespace.utils.json_to_sheet(rows);
9169
+ XLSX__namespace.utils.book_append_sheet(workbook, worksheet, "submissions");
9170
+ return XLSX__namespace.write(workbook, { bookType: "xlsx", type: "buffer" });
9171
+ }
9172
+ recalculateVoteCounts() {
9173
+ const counts = /* @__PURE__ */ new Map();
9174
+ for (const vote of this.votes) {
9175
+ counts.set(vote.submissionId, (counts.get(vote.submissionId) || 0) + 1);
9176
+ }
9177
+ for (const [id, submission] of this.submissions.entries()) {
9178
+ const nextCount = counts.get(id) || 0;
9179
+ if (submission.voteCount === nextCount) continue;
9180
+ this.submissions.set(id, {
9181
+ ...submission,
9182
+ voteCount: nextCount,
9183
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9184
+ });
9185
+ }
9186
+ }
9187
+ exportPersistenceState() {
9188
+ return {
9189
+ contest: this.contest,
9190
+ submissions: [...this.submissions.values()],
9191
+ votes: [...this.votes],
9192
+ announcements: [...this.announcements.values()],
9193
+ voterRestrictions: [...this.voterRestrictions.values()],
9194
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9195
+ };
9196
+ }
9197
+ importPersistenceState(state) {
9198
+ this.contest = state.contest;
9199
+ this.submissions.clear();
9200
+ this.announcements.clear();
9201
+ this.voterRestrictions.clear();
9202
+ this.votes.length = 0;
9203
+ for (const item of state.submissions) {
9204
+ this.submissions.set(item.id, item);
9205
+ }
9206
+ for (const item of state.announcements) {
9207
+ this.announcements.set(item.id, item);
9208
+ }
9209
+ for (const item of state.voterRestrictions) {
9210
+ this.voterRestrictions.set(item.voterId, item);
9211
+ }
9212
+ this.votes.push(...state.votes);
9213
+ }
9214
+ };
9215
+ var createMikuContestService = (options) => {
9216
+ return new MikuContestService(options);
9217
+ };
9218
+ var mikuContestConfigs = pgCore.pgTable("miku_contest_configs", {
9219
+ contestId: pgCore.text("contest_id").primaryKey(),
9220
+ config: pgCore.jsonb("config").$type().notNull(),
9221
+ createdAt: pgCore.timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
9222
+ updatedAt: pgCore.timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
9223
+ });
9224
+ var mikuContestSubmissions = pgCore.pgTable("miku_contest_submissions", {
9225
+ id: pgCore.text("id").primaryKey(),
9226
+ contestId: pgCore.text("contest_id").notNull(),
9227
+ serialNo: pgCore.text("serial_no").notNull(),
9228
+ authorId: pgCore.text("author_id").notNull(),
9229
+ authorNickname: pgCore.text("author_nickname").notNull(),
9230
+ title: pgCore.text("title").notNull(),
9231
+ type: pgCore.text("type").notNull(),
9232
+ description: pgCore.text("description").notNull(),
9233
+ tags: pgCore.jsonb("tags").$type().notNull(),
9234
+ content: pgCore.jsonb("content").$type().notNull(),
9235
+ voteCount: pgCore.integer("vote_count").notNull().default(0),
9236
+ status: pgCore.text("status").notNull(),
9237
+ rejectReason: pgCore.text("reject_reason"),
9238
+ createdAt: pgCore.timestamp("created_at", { withTimezone: true }).notNull(),
9239
+ reviewedAt: pgCore.timestamp("reviewed_at", { withTimezone: true }),
9240
+ updatedAt: pgCore.timestamp("updated_at", { withTimezone: true }).notNull()
9241
+ });
9242
+ var mikuContestVotes = pgCore.pgTable("miku_contest_votes", {
9243
+ id: pgCore.text("id").primaryKey(),
9244
+ contestId: pgCore.text("contest_id").notNull(),
9245
+ submissionId: pgCore.text("submission_id").notNull(),
9246
+ voterId: pgCore.text("voter_id").notNull(),
9247
+ votedAt: pgCore.text("voted_at").notNull(),
9248
+ dayKey: pgCore.text("day_key").notNull(),
9249
+ deviceId: pgCore.text("device_id"),
9250
+ ip: pgCore.text("ip")
9251
+ });
9252
+ var mikuContestNotices = pgCore.pgTable("miku_contest_notices", {
9253
+ id: pgCore.text("id").primaryKey(),
9254
+ contestId: pgCore.text("contest_id").notNull(),
9255
+ title: pgCore.text("title").notNull(),
9256
+ content: pgCore.text("content").notNull(),
9257
+ type: pgCore.text("type").notNull(),
9258
+ createdBy: pgCore.text("created_by").notNull(),
9259
+ createdAt: pgCore.text("created_at").notNull(),
9260
+ updatedAt: pgCore.text("updated_at").notNull()
9261
+ });
9262
+ var mikuContestVoterRestrictions = pgCore.pgTable("miku_contest_voter_restrictions", {
9263
+ id: pgCore.text("id").primaryKey(),
9264
+ contestId: pgCore.text("contest_id").notNull(),
9265
+ data: pgCore.jsonb("data").$type().notNull()
9266
+ });
9267
+ var MikuContestStateDbService = class {
9268
+ constructor(db) {
9269
+ this.db = db;
9270
+ }
9271
+ async loadState(contestId) {
9272
+ const configRows = await this.db.select({ config: mikuContestConfigs.config }).from(mikuContestConfigs).where(drizzleOrm.eq(mikuContestConfigs.contestId, contestId)).limit(1);
9273
+ const config = configRows[0]?.config;
9274
+ if (!config) return null;
9275
+ const [submissions, votes, announcements, restrictions] = await Promise.all([
9276
+ this.db.select().from(mikuContestSubmissions).where(drizzleOrm.eq(mikuContestSubmissions.contestId, contestId)),
9277
+ this.db.select().from(mikuContestVotes).where(drizzleOrm.eq(mikuContestVotes.contestId, contestId)),
9278
+ this.db.select().from(mikuContestNotices).where(drizzleOrm.eq(mikuContestNotices.contestId, contestId)),
9279
+ this.db.select({ data: mikuContestVoterRestrictions.data }).from(mikuContestVoterRestrictions).where(drizzleOrm.eq(mikuContestVoterRestrictions.contestId, contestId))
9280
+ ]);
9281
+ return {
9282
+ contest: config,
9283
+ submissions,
9284
+ votes,
9285
+ announcements,
9286
+ voterRestrictions: restrictions.map((item) => item.data),
9287
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9288
+ };
9289
+ }
9290
+ async saveState(state) {
9291
+ const contestId = state.contest.id;
9292
+ const exists = await this.db.select({ contestId: mikuContestConfigs.contestId }).from(mikuContestConfigs).where(drizzleOrm.eq(mikuContestConfigs.contestId, contestId)).limit(1);
9293
+ if (exists[0]) {
9294
+ await this.db.update(mikuContestConfigs).set({
9295
+ config: state.contest,
9296
+ updatedAt: /* @__PURE__ */ new Date()
9297
+ }).where(drizzleOrm.eq(mikuContestConfigs.contestId, contestId));
9298
+ } else {
9299
+ await this.db.insert(mikuContestConfigs).values({
9300
+ contestId,
9301
+ config: state.contest
9302
+ });
9303
+ }
9304
+ await this.db.delete(mikuContestSubmissions).where(drizzleOrm.eq(mikuContestSubmissions.contestId, contestId));
9305
+ await this.db.delete(mikuContestVotes).where(drizzleOrm.eq(mikuContestVotes.contestId, contestId));
9306
+ await this.db.delete(mikuContestNotices).where(drizzleOrm.eq(mikuContestNotices.contestId, contestId));
9307
+ await this.db.delete(mikuContestVoterRestrictions).where(drizzleOrm.eq(mikuContestVoterRestrictions.contestId, contestId));
9308
+ if (state.submissions.length > 0) {
9309
+ await this.db.insert(mikuContestSubmissions).values(
9310
+ state.submissions.map((item) => ({
9311
+ ...item,
9312
+ contestId,
9313
+ createdAt: new Date(item.createdAt),
9314
+ reviewedAt: item.reviewedAt ? new Date(item.reviewedAt) : null,
9315
+ updatedAt: new Date(item.updatedAt)
9316
+ }))
9317
+ );
9318
+ }
9319
+ if (state.votes.length > 0) {
9320
+ await this.db.insert(mikuContestVotes).values(state.votes);
9321
+ }
9322
+ if (state.announcements.length > 0) {
9323
+ await this.db.insert(mikuContestNotices).values(state.announcements);
9324
+ }
9325
+ if (state.voterRestrictions.length > 0) {
9326
+ await this.db.insert(mikuContestVoterRestrictions).values(
9327
+ state.voterRestrictions.map((item) => ({
9328
+ id: `${contestId}:${item.voterId}`,
9329
+ contestId,
9330
+ data: item
9331
+ }))
9332
+ );
9333
+ }
9334
+ }
9335
+ };
9336
+
9337
+ // src/mikuContest/server/persistence/drizzle-adapter.ts
9338
+ var createMikuContestDrizzlePersistenceAdapter = (db) => {
9339
+ const service = new MikuContestStateDbService(db);
9340
+ return {
9341
+ loadState: (contestId) => service.loadState(contestId),
9342
+ saveState: (state) => service.saveState(state)
9343
+ };
9344
+ };
9345
+
9346
+ // src/mikuContest/server/persistence/service.ts
9347
+ var MikuContestPersistentService = class {
9348
+ constructor(options) {
9349
+ this.options = options;
9350
+ this.hydrated = false;
9351
+ this.hydrationPromise = null;
9352
+ this.engine = createMikuContestService(options);
9353
+ }
9354
+ async ensureHydrated() {
9355
+ if (this.hydrated) return;
9356
+ if (this.hydrationPromise) return this.hydrationPromise;
9357
+ this.hydrationPromise = (async () => {
9358
+ const contestId = this.engine.getContestConfig().id;
9359
+ const loaded = await this.options.persistenceAdapter.loadState(contestId);
9360
+ if (loaded) {
9361
+ this.engine.importPersistenceState(loaded);
9362
+ } else {
9363
+ await this.options.persistenceAdapter.saveState(this.engine.exportPersistenceState());
9364
+ }
9365
+ this.hydrated = true;
9366
+ })();
9367
+ await this.hydrationPromise;
9368
+ }
9369
+ async persist() {
9370
+ await this.options.persistenceAdapter.saveState(this.engine.exportPersistenceState());
9371
+ }
9372
+ async getContestConfig() {
9373
+ await this.ensureHydrated();
9374
+ return this.engine.getContestConfig();
9375
+ }
9376
+ async updateContestConfig(patch) {
9377
+ await this.ensureHydrated();
9378
+ const data = this.engine.updateContestConfig(patch);
9379
+ await this.persist();
9380
+ return data;
9381
+ }
9382
+ async createSubmission(input, mode = "web") {
9383
+ await this.ensureHydrated();
9384
+ const data = this.engine.createSubmission(input, mode);
9385
+ await this.persist();
9386
+ return data;
9387
+ }
9388
+ async listSubmissions(filter) {
9389
+ await this.ensureHydrated();
9390
+ return this.engine.listSubmissions(filter);
9391
+ }
9392
+ async getSubmission(submissionId) {
9393
+ await this.ensureHydrated();
9394
+ return this.engine.getSubmission(submissionId);
9395
+ }
9396
+ async reviewSubmission(input) {
9397
+ await this.ensureHydrated();
9398
+ const data = this.engine.reviewSubmission(input);
9399
+ await this.persist();
9400
+ return data;
9401
+ }
9402
+ async vote(input) {
9403
+ await this.ensureHydrated();
9404
+ const data = this.engine.vote(input);
9405
+ await this.persist();
9406
+ return data;
9407
+ }
9408
+ async getVoterRestriction(voterId) {
9409
+ await this.ensureHydrated();
9410
+ return this.engine.getVoterRestriction(voterId);
9411
+ }
9412
+ async setVoterRestriction(input) {
9413
+ await this.ensureHydrated();
9414
+ const data = this.engine.setVoterRestriction(input);
9415
+ await this.persist();
9416
+ return data;
9417
+ }
9418
+ async resetVotes(input) {
9419
+ await this.ensureHydrated();
9420
+ const data = this.engine.resetVotes(input);
9421
+ await this.persist();
9422
+ return data;
9423
+ }
9424
+ async listAnnouncements(contestId) {
9425
+ await this.ensureHydrated();
9426
+ return this.engine.listAnnouncements(contestId);
9427
+ }
9428
+ async publishAnnouncement(input) {
9429
+ await this.ensureHydrated();
9430
+ const data = this.engine.publishAnnouncement(input);
9431
+ await this.persist();
9432
+ return data;
9433
+ }
9434
+ async getLeaderboard(limit = 10) {
9435
+ await this.ensureHydrated();
9436
+ return this.engine.getLeaderboard(limit);
9437
+ }
9438
+ async getSnapshot() {
9439
+ await this.ensureHydrated();
9440
+ return this.engine.getSnapshot();
9441
+ }
9442
+ async getSubmissionExportRows(filter) {
9443
+ await this.ensureHydrated();
9444
+ return this.engine.getSubmissionExportRows(filter);
9445
+ }
9446
+ async exportSubmissionExcel(filter) {
9447
+ await this.ensureHydrated();
9448
+ return this.engine.exportSubmissionExcel(filter);
9449
+ }
9450
+ };
9451
+ var createMikuContestPersistentService = (options) => {
9452
+ return new MikuContestPersistentService(options);
9453
+ };
9454
+
9455
+ // src/mikuContest/server/db.ts
9456
+ var MikuContestDbService = class {
9457
+ constructor() {
9458
+ this._db = null;
9459
+ }
9460
+ setDb(db) {
9461
+ this._db = db;
9462
+ }
9463
+ isConfigured() {
9464
+ return Boolean(this._db);
9465
+ }
9466
+ get db() {
9467
+ if (!this._db) {
9468
+ throw new Error("MikuContestDbService: Database instance not set. Call setDb(db) first.");
9469
+ }
9470
+ return this._db;
9471
+ }
9472
+ };
9473
+ var mikuContestDbService = new MikuContestDbService();
9474
+
9475
+ // src/mikuContest/logic/hooks/useMikuContest.ts
9476
+ var useMikuContest = (options) => {
9477
+ const [service] = React69.useState(() => createMikuContestService(options));
9478
+ const [version, setVersion] = React69.useState(0);
9479
+ const refresh = () => setVersion((value) => value + 1);
9480
+ const snapshot = React69.useMemo(() => {
9481
+ return service.getSnapshot();
9482
+ }, [service, version]);
9483
+ return {
9484
+ service,
9485
+ snapshot,
9486
+ refresh
9487
+ };
9488
+ };
9489
+
9490
+ // src/mikuContest/service/api/client.ts
9491
+ var toQueryString = (filter) => {
9492
+ if (!filter) return "";
9493
+ const params = new URLSearchParams();
9494
+ if (filter.status) params.set("status", filter.status);
9495
+ if (filter.type) params.set("type", filter.type);
9496
+ if (filter.authorId) params.set("authorId", filter.authorId);
9497
+ if (filter.authorKeyword) params.set("authorKeyword", filter.authorKeyword);
9498
+ if (filter.titleKeyword) params.set("titleKeyword", filter.titleKeyword);
9499
+ const query = params.toString();
9500
+ return query ? `?${query}` : "";
9501
+ };
9502
+ var unwrap = (result) => {
9503
+ if (!result.success || result.data === void 0) {
9504
+ throw new Error(result.error || "\u8BF7\u6C42\u5931\u8D25");
9505
+ }
9506
+ return result.data;
9507
+ };
9508
+ var createMikuContestApiClient = (basePath, requester) => {
9509
+ return {
9510
+ async getSnapshot() {
9511
+ const result = await requester(`${basePath}/contest`, { method: "GET" });
9512
+ return unwrap(result);
9513
+ },
9514
+ async updateContestConfig(patch) {
9515
+ const result = await requester(`${basePath}/contest`, {
9516
+ method: "PATCH",
9517
+ body: patch
9518
+ });
9519
+ return unwrap(result);
9520
+ },
9521
+ async createSubmission(input, mode = "web") {
9522
+ const result = await requester(`${basePath}/submissions`, {
9523
+ method: "POST",
9524
+ body: { payload: input, mode }
9525
+ });
9526
+ return unwrap(result);
9527
+ },
9528
+ async listSubmissions(filter) {
9529
+ const result = await requester(
9530
+ `${basePath}/submissions${toQueryString(filter)}`,
9531
+ { method: "GET" }
9532
+ );
9533
+ return unwrap(result);
9534
+ },
9535
+ async reviewSubmission(input) {
9536
+ const result = await requester(`${basePath}/submissions/review`, {
9537
+ method: "POST",
9538
+ body: input
9539
+ });
9540
+ return unwrap(result);
9541
+ },
9542
+ async vote(input) {
9543
+ const result = await requester(`${basePath}/votes`, {
9544
+ method: "POST",
9545
+ body: input
9546
+ });
9547
+ return unwrap(result);
9548
+ },
9549
+ async setVoterRestriction(input) {
9550
+ const result = await requester(`${basePath}/admin/voter-restrictions`, {
9551
+ method: "POST",
9552
+ body: input
9553
+ });
9554
+ return unwrap(result);
9555
+ },
9556
+ async resetVotes(input) {
9557
+ const result = await requester(
9558
+ `${basePath}/admin/votes/reset`,
9559
+ {
9560
+ method: "POST",
9561
+ body: input
9562
+ }
9563
+ );
9564
+ return unwrap(result);
9565
+ },
9566
+ async exportSubmissions(filter) {
9567
+ const response = await fetch(`${basePath}/admin/submissions/export${toQueryString(filter)}`);
9568
+ if (!response.ok) {
9569
+ throw new Error(`\u5BFC\u51FA\u5931\u8D25: ${response.status}`);
9570
+ }
9571
+ return response.arrayBuffer();
9572
+ }
9573
+ };
9574
+ };
9575
+
9576
+ // src/mikuContest/service/web/index.ts
9577
+ var web_exports = {};
9578
+ __export(web_exports, {
9579
+ createMikuContestWebClient: () => createMikuContestWebClient
9580
+ });
9581
+
9582
+ // src/mikuContest/service/web/client.ts
9583
+ var defaultRequester = (options) => {
9584
+ const baseUrl = options.baseUrl || "";
9585
+ const commonHeaders = options.headers || {};
9586
+ return async (url, requestOptions) => {
9587
+ const response = await fetch(`${baseUrl}${url}`, {
9588
+ method: requestOptions?.method || "GET",
9589
+ headers: {
9590
+ "Content-Type": "application/json",
9591
+ ...commonHeaders
9592
+ },
9593
+ body: requestOptions?.body ? JSON.stringify(requestOptions.body) : void 0
9594
+ });
9595
+ const json = await response.json();
9596
+ return json;
9597
+ };
9598
+ };
9599
+ var createMikuContestWebClient = (options = {}) => {
9600
+ const basePath = options.basePath || "/api/miku-contest";
9601
+ return createMikuContestApiClient(basePath, defaultRequester(options));
9602
+ };
9603
+
9604
+ // src/mikuContest/service/miniapp/index.ts
9605
+ var miniapp_exports = {};
9606
+ __export(miniapp_exports, {
9607
+ createMikuContestMiniappClient: () => createMikuContestMiniappClient
9608
+ });
9609
+
9610
+ // src/mikuContest/service/miniapp/client.ts
9611
+ var createMikuContestMiniappClient = (options) => {
9612
+ const basePath = options.basePath || "/api/miku-contest";
9613
+ return createMikuContestApiClient(basePath, options.requester);
9614
+ };
9615
+ var isDrizzleDb = (value) => {
9616
+ if (!value || typeof value !== "object") return false;
9617
+ const candidate = value;
9618
+ return typeof candidate.select === "function" && typeof candidate.insert === "function" && typeof candidate.update === "function" && typeof candidate.delete === "function";
9619
+ };
9620
+ var resolveService = (config) => {
9621
+ if (config?.service) return config.service;
9622
+ const adapter = config?.persistenceAdapter || (isDrizzleDb(config?.db) ? createMikuContestDrizzlePersistenceAdapter(config.db) : null);
9623
+ if (adapter) {
9624
+ return createMikuContestPersistentService({
9625
+ persistenceAdapter: adapter
9626
+ });
9627
+ }
9628
+ return createMikuContestService();
9629
+ };
9630
+ var createGetContestSnapshotHandler = (config) => {
9631
+ const service = resolveService(config);
9632
+ return async (_request) => {
9633
+ const data = await service.getSnapshot();
9634
+ return server.NextResponse.json({ success: true, data });
9635
+ };
9636
+ };
9637
+ var createUpdateContestConfigHandler = (config) => {
9638
+ const service = resolveService(config);
9639
+ return async (request) => {
9640
+ try {
9641
+ const payload = await request.json();
9642
+ const data = await service.updateContestConfig(payload);
9643
+ return server.NextResponse.json({ success: true, data });
9644
+ } catch (error) {
9645
+ return server.NextResponse.json({ success: false, error: error.message }, { status: 400 });
9646
+ }
9647
+ };
9648
+ };
9649
+ var createCreateSubmissionHandler = (config) => {
9650
+ const service = resolveService(config);
9651
+ return async (request) => {
9652
+ try {
9653
+ const body = await request.json();
9654
+ const mode = body.mode || "web";
9655
+ const payload = body.payload;
9656
+ if (!payload) {
9657
+ return server.NextResponse.json({ success: false, error: "payload \u4E0D\u80FD\u4E3A\u7A7A" }, { status: 400 });
9658
+ }
9659
+ const data = await service.createSubmission(payload, mode);
9660
+ return server.NextResponse.json({ success: true, data });
9661
+ } catch (error) {
9662
+ return server.NextResponse.json({ success: false, error: error.message }, { status: 400 });
9663
+ }
9664
+ };
9665
+ };
9666
+ var createVoteHandler = (config) => {
9667
+ const service = resolveService(config);
9668
+ return async (request) => {
9669
+ try {
9670
+ const payload = await request.json();
9671
+ const data = await service.vote(payload);
9672
+ return server.NextResponse.json({ success: true, data });
9673
+ } catch (error) {
9674
+ return server.NextResponse.json({ success: false, error: error.message }, { status: 400 });
9675
+ }
9676
+ };
9677
+ };
9678
+ var createReviewSubmissionHandler = (config) => {
9679
+ const service = resolveService(config);
9680
+ return async (request) => {
9681
+ try {
9682
+ const payload = await request.json();
9683
+ const data = await service.reviewSubmission(payload);
9684
+ return server.NextResponse.json({ success: true, data });
9685
+ } catch (error) {
9686
+ return server.NextResponse.json({ success: false, error: error.message }, { status: 400 });
9687
+ }
9688
+ };
9689
+ };
9690
+ var buildSubmissionFilterFromQuery = (request) => {
9691
+ const search = request.nextUrl.searchParams;
9692
+ const status = search.get("status");
9693
+ const type = search.get("type");
9694
+ return {
9695
+ status: status ? status : void 0,
9696
+ type: type ? type : void 0,
9697
+ authorId: search.get("authorId") || void 0,
9698
+ authorKeyword: search.get("authorKeyword") || void 0,
9699
+ titleKeyword: search.get("titleKeyword") || void 0
9700
+ };
9701
+ };
9702
+ var createListSubmissionsHandler = (config) => {
9703
+ const service = resolveService(config);
9704
+ return async (request) => {
9705
+ const filter = buildSubmissionFilterFromQuery(request);
9706
+ const data = await service.listSubmissions(filter);
9707
+ return server.NextResponse.json({ success: true, data });
9708
+ };
9709
+ };
9710
+ var createSetVoterRestrictionHandler = (config) => {
9711
+ const service = resolveService(config);
9712
+ return async (request) => {
9713
+ try {
9714
+ const payload = await request.json();
9715
+ const data = await service.setVoterRestriction(payload);
9716
+ return server.NextResponse.json({ success: true, data });
9717
+ } catch (error) {
9718
+ return server.NextResponse.json({ success: false, error: error.message }, { status: 400 });
9719
+ }
9720
+ };
9721
+ };
9722
+ var createResetVotesHandler = (config) => {
9723
+ const service = resolveService(config);
9724
+ return async (request) => {
9725
+ try {
9726
+ const payload = await request.json();
9727
+ const data = await service.resetVotes(payload);
9728
+ return server.NextResponse.json({ success: true, data });
9729
+ } catch (error) {
9730
+ return server.NextResponse.json({ success: false, error: error.message }, { status: 400 });
9731
+ }
9732
+ };
9733
+ };
9734
+ var createExportSubmissionsHandler = (config) => {
9735
+ const service = resolveService(config);
9736
+ return async (request) => {
9737
+ const filter = buildSubmissionFilterFromQuery(request);
9738
+ const data = await service.exportSubmissionExcel(filter);
9739
+ const body = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
9740
+ return new server.NextResponse(body, {
9741
+ status: 200,
9742
+ headers: {
9743
+ "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
9744
+ "Content-Disposition": 'attachment; filename="miku-submissions.xlsx"'
9745
+ }
9746
+ });
9747
+ };
9748
+ };
9749
+
9750
+ // src/mikuContest/ui/web/index.ts
9751
+ var web_exports2 = {};
9752
+ __export(web_exports2, {
9753
+ MikuContestAdminPage: () => MikuContestAdminPage_default,
9754
+ MikuContestArtistPage: () => MikuContestArtistPage_default,
9755
+ MikuContestAudiencePage: () => MikuContestAudiencePage_default,
9756
+ MikuContestDashboard: () => MikuContestDashboard_default,
9757
+ MikuContestPage: () => MikuContestPage_default
9758
+ });
9759
+ var MikuContestDashboard = ({ snapshot }) => {
9760
+ return /* @__PURE__ */ React69__namespace.default.createElement("div", null, /* @__PURE__ */ React69__namespace.default.createElement("h2", null, snapshot.contest.name), /* @__PURE__ */ React69__namespace.default.createElement("p", null, snapshot.contest.theme), /* @__PURE__ */ React69__namespace.default.createElement("p", null, "\u6295\u7A3F\u6570\uFF1A", snapshot.submissions.length), /* @__PURE__ */ React69__namespace.default.createElement("p", null, "\u516C\u544A\u6570\uFF1A", snapshot.announcements.length), /* @__PURE__ */ React69__namespace.default.createElement("ul", null, snapshot.leaderboard.map((item) => /* @__PURE__ */ React69__namespace.default.createElement("li", { key: item.submissionId }, "#", item.rank, " ", item.title, " - ", item.voteCount, "\u7968"))));
9761
+ };
9762
+ var MikuContestDashboard_default = MikuContestDashboard;
9763
+ var MikuContestAudiencePage = ({
9764
+ client,
9765
+ voterId,
9766
+ title = "\u89C2\u4F17\u6295\u7968\u533A"
9767
+ }) => {
9768
+ const api = React69.useMemo(() => client || createMikuContestWebClient(), [client]);
9769
+ const [snapshot, setSnapshot] = React69.useState(null);
9770
+ const [loading, setLoading] = React69.useState(false);
9771
+ const [error, setError] = React69.useState(null);
9772
+ const approvedWorks = React69.useMemo(() => {
9773
+ if (!snapshot) return [];
9774
+ return snapshot.submissions.filter((item) => item.status === "approved");
9775
+ }, [snapshot]);
9776
+ const loadSnapshot = async () => {
9777
+ setLoading(true);
9778
+ setError(null);
9779
+ try {
9780
+ const data = await api.getSnapshot();
9781
+ setSnapshot(data);
9782
+ } catch (e) {
9783
+ setError(e.message);
9784
+ } finally {
9785
+ setLoading(false);
9786
+ }
9787
+ };
9788
+ React69.useEffect(() => {
9789
+ void loadSnapshot();
9790
+ }, []);
9791
+ const handleVote = async (submission) => {
9792
+ if (!snapshot) return;
9793
+ try {
9794
+ await api.vote({
9795
+ contestId: snapshot.contest.id,
9796
+ submissionId: submission.id,
9797
+ voterId
9798
+ });
9799
+ await loadSnapshot();
9800
+ } catch (e) {
9801
+ setError(e.message);
9802
+ }
9803
+ };
9804
+ return /* @__PURE__ */ React69__namespace.default.createElement("section", null, /* @__PURE__ */ React69__namespace.default.createElement("h2", null, title), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => void loadSnapshot(), disabled: loading }, loading ? "\u5237\u65B0\u4E2D..." : "\u5237\u65B0\u6570\u636E"), error ? /* @__PURE__ */ React69__namespace.default.createElement("p", { style: { color: "crimson" } }, "\u9519\u8BEF\uFF1A", error) : null, !snapshot ? null : /* @__PURE__ */ React69__namespace.default.createElement(React69__namespace.default.Fragment, null, /* @__PURE__ */ React69__namespace.default.createElement("p", null, "\u8D5B\u4E8B\uFF1A", snapshot.contest.name, "\uFF5C\u4E3B\u9898\uFF1A", snapshot.contest.theme), /* @__PURE__ */ React69__namespace.default.createElement("p", null, "\u5DF2\u8FC7\u5BA1\u4F5C\u54C1\uFF1A", approvedWorks.length, "\uFF5C\u6BCF\u65E5\u4E0A\u9650\uFF1A", snapshot.contest.votingRules.maxVotesPerDay), /* @__PURE__ */ React69__namespace.default.createElement("ul", null, approvedWorks.map((work) => /* @__PURE__ */ React69__namespace.default.createElement("li", { key: work.id }, /* @__PURE__ */ React69__namespace.default.createElement("strong", null, work.title), "\uFF08", work.authorNickname, "\uFF09- \u5F53\u524D ", work.voteCount, " \u7968", " ", /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => void handleVote(work) }, "\u6295\u7968"))))));
9805
+ };
9806
+ var MikuContestAudiencePage_default = MikuContestAudiencePage;
9807
+ var workTypes = ["visual", "video", "text", "audio"];
9808
+ var MikuContestArtistPage = ({
9809
+ client,
9810
+ authorId,
9811
+ authorNickname,
9812
+ title = "\u753B\u5E08\u6295\u7A3F\u533A"
9813
+ }) => {
9814
+ const api = React69.useMemo(() => client || createMikuContestWebClient(), [client]);
9815
+ const [snapshot, setSnapshot] = React69.useState(null);
9816
+ const [mySubmissions, setMySubmissions] = React69.useState([]);
9817
+ const [submitting, setSubmitting] = React69.useState(false);
9818
+ const [loading, setLoading] = React69.useState(false);
9819
+ const [error, setError] = React69.useState(null);
9820
+ const [titleInput, setTitleInput] = React69.useState("");
9821
+ const [descInput, setDescInput] = React69.useState("");
9822
+ const [coverImage, setCoverImage] = React69.useState("");
9823
+ const [workType, setWorkType] = React69.useState("visual");
9824
+ const loadData = async () => {
9825
+ setLoading(true);
9826
+ setError(null);
9827
+ try {
9828
+ const [contest, mine] = await Promise.all([
9829
+ api.getSnapshot(),
9830
+ api.listSubmissions({ authorId })
9831
+ ]);
9832
+ setSnapshot(contest);
9833
+ setMySubmissions(mine);
9834
+ } catch (e) {
9835
+ setError(e.message);
9836
+ } finally {
9837
+ setLoading(false);
9838
+ }
9839
+ };
9840
+ React69.useEffect(() => {
9841
+ void loadData();
9842
+ }, []);
9843
+ const submitWork = async () => {
9844
+ if (!snapshot) return;
9845
+ const payload = {
9846
+ contestId: snapshot.contest.id,
9847
+ authorId,
9848
+ authorNickname,
9849
+ title: titleInput,
9850
+ description: descInput,
9851
+ type: workType,
9852
+ tags: ["web"],
9853
+ content: {
9854
+ coverImage,
9855
+ images: coverImage ? [coverImage] : void 0
9856
+ }
9857
+ };
9858
+ setSubmitting(true);
9859
+ setError(null);
9860
+ try {
9861
+ await api.createSubmission(payload, "web");
9862
+ setTitleInput("");
9863
+ setDescInput("");
9864
+ setCoverImage("");
9865
+ await loadData();
9866
+ } catch (e) {
9867
+ setError(e.message);
9868
+ } finally {
9869
+ setSubmitting(false);
9870
+ }
9871
+ };
9872
+ return /* @__PURE__ */ React69__namespace.default.createElement("section", null, /* @__PURE__ */ React69__namespace.default.createElement("h2", null, title), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => void loadData(), disabled: loading }, loading ? "\u5237\u65B0\u4E2D..." : "\u5237\u65B0\u6570\u636E"), error ? /* @__PURE__ */ React69__namespace.default.createElement("p", { style: { color: "crimson" } }, "\u9519\u8BEF\uFF1A", error) : null, /* @__PURE__ */ React69__namespace.default.createElement("div", null, /* @__PURE__ */ React69__namespace.default.createElement("h3", null, "\u65B0\u5EFA\u6295\u7A3F"), /* @__PURE__ */ React69__namespace.default.createElement("input", { value: titleInput, onChange: (e) => setTitleInput(e.target.value), placeholder: "\u4F5C\u54C1\u6807\u9898" }), /* @__PURE__ */ React69__namespace.default.createElement("br", null), /* @__PURE__ */ React69__namespace.default.createElement("textarea", { value: descInput, onChange: (e) => setDescInput(e.target.value), placeholder: "\u4F5C\u54C1\u7B80\u4ECB" }), /* @__PURE__ */ React69__namespace.default.createElement("br", null), /* @__PURE__ */ React69__namespace.default.createElement("input", { value: coverImage, onChange: (e) => setCoverImage(e.target.value), placeholder: "\u5C01\u9762 URL" }), /* @__PURE__ */ React69__namespace.default.createElement("br", null), /* @__PURE__ */ React69__namespace.default.createElement("select", { value: workType, onChange: (e) => setWorkType(e.target.value) }, workTypes.map((item) => /* @__PURE__ */ React69__namespace.default.createElement("option", { value: item, key: item }, item))), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => void submitWork(), disabled: submitting || !snapshot }, submitting ? "\u63D0\u4EA4\u4E2D..." : "\u63D0\u4EA4\u7A3F\u4EF6")), /* @__PURE__ */ React69__namespace.default.createElement("div", null, /* @__PURE__ */ React69__namespace.default.createElement("h3", null, "\u6211\u7684\u6295\u7A3F\uFF08", mySubmissions.length, "\uFF09"), /* @__PURE__ */ React69__namespace.default.createElement("ul", null, mySubmissions.map((item) => /* @__PURE__ */ React69__namespace.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}` : "")))));
9873
+ };
9874
+ var MikuContestArtistPage_default = MikuContestArtistPage;
9875
+ var MikuContestAdminPage = ({
9876
+ client,
9877
+ adminId,
9878
+ title = "\u7BA1\u7406\u5458\u9762\u677F"
9879
+ }) => {
9880
+ const api = React69.useMemo(() => client || createMikuContestWebClient(), [client]);
9881
+ const [snapshot, setSnapshot] = React69.useState(null);
9882
+ const [submissions, setSubmissions] = React69.useState([]);
9883
+ const [loading, setLoading] = React69.useState(false);
9884
+ const [error, setError] = React69.useState(null);
9885
+ const [voterId, setVoterId] = React69.useState("");
9886
+ const loadData = async () => {
9887
+ setLoading(true);
9888
+ setError(null);
9889
+ try {
9890
+ const [contest, list] = await Promise.all([api.getSnapshot(), api.listSubmissions()]);
9891
+ setSnapshot(contest);
9892
+ setSubmissions(list);
9893
+ } catch (e) {
9894
+ setError(e.message);
9895
+ } finally {
9896
+ setLoading(false);
9897
+ }
9898
+ };
9899
+ React69.useEffect(() => {
9900
+ void loadData();
9901
+ }, []);
9902
+ const review = async (item, action) => {
9903
+ try {
9904
+ await api.reviewSubmission({
9905
+ submissionId: item.id,
9906
+ reviewerId: adminId,
9907
+ action,
9908
+ rejectReason: action === "reject" ? "\u7BA1\u7406\u5458\u9A73\u56DE" : void 0
9909
+ });
9910
+ await loadData();
9911
+ } catch (e) {
9912
+ setError(e.message);
9913
+ }
9914
+ };
9915
+ const toggleVoting = async (enabled) => {
9916
+ if (!snapshot) return;
9917
+ try {
9918
+ await api.updateContestConfig({
9919
+ toggles: {
9920
+ ...snapshot.contest.toggles,
9921
+ votingEnabled: enabled
9922
+ }
9923
+ });
9924
+ await loadData();
9925
+ } catch (e) {
9926
+ setError(e.message);
9927
+ }
9928
+ };
9929
+ const setRestriction = async (banned) => {
9930
+ if (!snapshot || !voterId.trim()) return;
9931
+ try {
9932
+ await api.setVoterRestriction({
9933
+ voterId: voterId.trim(),
9934
+ banned,
9935
+ reason: banned ? "\u7BA1\u7406\u5458\u624B\u52A8\u5C01\u7981" : "\u7BA1\u7406\u5458\u89E3\u9664\u5C01\u7981",
9936
+ operatorId: adminId
9937
+ });
9938
+ setVoterId("");
9939
+ } catch (e) {
9940
+ setError(e.message);
9941
+ }
9942
+ };
9943
+ const resetVotesByVoter = async () => {
9944
+ if (!voterId.trim()) return;
9945
+ try {
9946
+ await api.resetVotes({ voterId: voterId.trim() });
9947
+ setVoterId("");
9948
+ await loadData();
9949
+ } catch (e) {
9950
+ setError(e.message);
9951
+ }
9952
+ };
9953
+ const exportExcel = async () => {
9954
+ try {
9955
+ const data = await api.exportSubmissions();
9956
+ const blob = new Blob([data], {
9957
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
9958
+ });
9959
+ const url = URL.createObjectURL(blob);
9960
+ const a = document.createElement("a");
9961
+ a.href = url;
9962
+ a.download = "miku-submissions.xlsx";
9963
+ a.click();
9964
+ URL.revokeObjectURL(url);
9965
+ } catch (e) {
9966
+ setError(e.message);
9967
+ }
9968
+ };
9969
+ return /* @__PURE__ */ React69__namespace.default.createElement("section", null, /* @__PURE__ */ React69__namespace.default.createElement("h2", null, title), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => void loadData(), disabled: loading }, loading ? "\u5237\u65B0\u4E2D..." : "\u5237\u65B0\u6570\u636E"), error ? /* @__PURE__ */ React69__namespace.default.createElement("p", { style: { color: "crimson" } }, "\u9519\u8BEF\uFF1A", error) : null, /* @__PURE__ */ React69__namespace.default.createElement("div", null, /* @__PURE__ */ React69__namespace.default.createElement("h3", null, "\u8D5B\u4E8B\u5F00\u5173"), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => void toggleVoting(true), disabled: !snapshot }, "\u5F00\u542F\u6295\u7968"), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => void toggleVoting(false), disabled: !snapshot }, "\u5173\u95ED\u6295\u7968")), /* @__PURE__ */ React69__namespace.default.createElement("div", null, /* @__PURE__ */ React69__namespace.default.createElement("h3", null, "\u6295\u7A3F\u5BA1\u6838\uFF08", submissions.length, "\uFF09"), /* @__PURE__ */ React69__namespace.default.createElement("ul", null, submissions.map((item) => /* @__PURE__ */ React69__namespace.default.createElement("li", { key: item.id }, /* @__PURE__ */ React69__namespace.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__namespace.default.createElement(React69__namespace.default.Fragment, null, " ", /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => void review(item, "approve") }, "\u901A\u8FC7"), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => void review(item, "reject") }, "\u9A73\u56DE")) : null)))), /* @__PURE__ */ React69__namespace.default.createElement("div", null, /* @__PURE__ */ React69__namespace.default.createElement("h3", null, "\u6295\u7968\u98CE\u63A7"), /* @__PURE__ */ React69__namespace.default.createElement("input", { value: voterId, onChange: (e) => setVoterId(e.target.value), placeholder: "voterId" }), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => void setRestriction(true), disabled: !snapshot }, "\u5C01\u7981\u6295\u7968"), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => void setRestriction(false), disabled: !snapshot }, "\u89E3\u9664\u5C01\u7981"), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => void resetVotesByVoter() }, "\u6E05\u96F6\u8BE5\u7528\u6237\u7968\u6570")), /* @__PURE__ */ React69__namespace.default.createElement("div", null, /* @__PURE__ */ React69__namespace.default.createElement("h3", null, "\u5BFC\u51FA"), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => void exportExcel() }, "\u5BFC\u51FA\u6295\u7A3F Excel")));
9970
+ };
9971
+ var MikuContestAdminPage_default = MikuContestAdminPage;
9972
+
9973
+ // src/mikuContest/ui/web/pages/MikuContestPage.tsx
9974
+ var MikuContestPage = ({
9975
+ defaultView = "audience",
9976
+ viewerVoterId = "viewer-demo",
9977
+ artistId = "artist-demo",
9978
+ artistNickname = "Demo \u753B\u5E08",
9979
+ adminId = "admin-demo"
9980
+ }) => {
9981
+ const [view, setView] = React69.useState(defaultView);
9982
+ return /* @__PURE__ */ React69__namespace.default.createElement("div", null, /* @__PURE__ */ React69__namespace.default.createElement("h1", null, "Miku Contest"), /* @__PURE__ */ React69__namespace.default.createElement("div", null, /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => setView("audience") }, "\u89C2\u4F17\u7AEF"), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => setView("artist") }, "\u753B\u5E08\u7AEF"), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => setView("admin") }, "\u7BA1\u7406\u5458\u7AEF")), view === "audience" ? /* @__PURE__ */ React69__namespace.default.createElement(MikuContestAudiencePage_default, { voterId: viewerVoterId }) : null, view === "artist" ? /* @__PURE__ */ React69__namespace.default.createElement(MikuContestArtistPage_default, { authorId: artistId, authorNickname: artistNickname }) : null, view === "admin" ? /* @__PURE__ */ React69__namespace.default.createElement(MikuContestAdminPage_default, { adminId }) : null);
9983
+ };
9984
+ var MikuContestPage_default = MikuContestPage;
9985
+
9986
+ // src/mikuContest/ui/miniapp/index.ts
9987
+ var miniapp_exports2 = {};
9988
+ __export(miniapp_exports2, {
9989
+ MikuContestMiniappHome: () => MikuContestMiniappHome_default,
9990
+ MikuContestMiniappPage: () => MikuContestMiniappPage_default
9991
+ });
9992
+ var MikuContestMiniappHome = ({ snapshot }) => {
9993
+ return /* @__PURE__ */ React69__namespace.default.createElement("div", null, /* @__PURE__ */ React69__namespace.default.createElement("h3", null, snapshot.contest.name), /* @__PURE__ */ React69__namespace.default.createElement("p", null, "\u6295\u7A3F\uFF1A", snapshot.submissions.length, " | \u516C\u544A\uFF1A", snapshot.announcements.length), /* @__PURE__ */ React69__namespace.default.createElement("ol", null, snapshot.leaderboard.slice(0, 3).map((item) => /* @__PURE__ */ React69__namespace.default.createElement("li", { key: item.submissionId }, item.title, "\uFF08", item.voteCount, "\u7968\uFF09"))));
9994
+ };
9995
+ var MikuContestMiniappHome_default = MikuContestMiniappHome;
9996
+ var MikuContestMiniappPage = () => {
9997
+ const { service, snapshot, refresh } = useMikuContest();
9998
+ const [tab, setTab] = React69.useState("vote");
9999
+ const [voterId, setVoterId] = React69.useState("miniapp-voter");
10000
+ const [authorId, setAuthorId] = React69.useState("miniapp-author");
10001
+ const [authorNickname, setAuthorNickname] = React69.useState("\u5C0F\u7A0B\u5E8F\u753B\u5E08");
10002
+ const [title, setTitle] = React69.useState("");
10003
+ const [desc, setDesc] = React69.useState("");
10004
+ const [type, setType] = React69.useState("visual");
10005
+ const [error, setError] = React69.useState(null);
10006
+ const approvedWorks = React69.useMemo(() => {
10007
+ return snapshot.submissions.filter((item) => item.status === "approved");
10008
+ }, [snapshot.submissions]);
10009
+ const vote = (submissionId) => {
10010
+ try {
10011
+ service.vote({
10012
+ contestId: snapshot.contest.id,
10013
+ submissionId,
10014
+ voterId
10015
+ });
10016
+ refresh();
10017
+ setError(null);
10018
+ } catch (e) {
10019
+ setError(e.message);
10020
+ }
10021
+ };
10022
+ const submit = () => {
10023
+ try {
10024
+ service.createSubmission(
10025
+ {
10026
+ contestId: snapshot.contest.id,
10027
+ authorId,
10028
+ authorNickname,
10029
+ title,
10030
+ description: desc,
10031
+ type,
10032
+ content: {}
10033
+ },
10034
+ "miniapp"
10035
+ );
10036
+ setTitle("");
10037
+ setDesc("");
10038
+ refresh();
10039
+ setError(null);
10040
+ } catch (e) {
10041
+ setError(e.message);
10042
+ }
10043
+ };
10044
+ return /* @__PURE__ */ React69__namespace.default.createElement("div", null, /* @__PURE__ */ React69__namespace.default.createElement("h3", null, snapshot.contest.name), /* @__PURE__ */ React69__namespace.default.createElement("p", null, "\u5C0F\u7A0B\u5E8F\u7AEF\u793A\u4F8B\u9875\u9762"), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => setTab("vote") }, "\u89C2\u4F17\u6295\u7968"), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => setTab("submit") }, "\u753B\u5E08\u6295\u7A3F"), error ? /* @__PURE__ */ React69__namespace.default.createElement("p", { style: { color: "crimson" } }, "\u9519\u8BEF\uFF1A", error) : null, tab === "vote" ? /* @__PURE__ */ React69__namespace.default.createElement("div", null, /* @__PURE__ */ React69__namespace.default.createElement("input", { value: voterId, onChange: (e) => setVoterId(e.target.value), placeholder: "voterId" }), /* @__PURE__ */ React69__namespace.default.createElement("ul", null, approvedWorks.map((item) => /* @__PURE__ */ React69__namespace.default.createElement("li", { key: item.id }, item.title, "\uFF08", item.voteCount, "\u7968\uFF09", /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: () => vote(item.id) }, "\u6295\u7968"))))) : null, tab === "submit" ? /* @__PURE__ */ React69__namespace.default.createElement("div", null, /* @__PURE__ */ React69__namespace.default.createElement("input", { value: authorId, onChange: (e) => setAuthorId(e.target.value), placeholder: "authorId" }), /* @__PURE__ */ React69__namespace.default.createElement("input", { value: authorNickname, onChange: (e) => setAuthorNickname(e.target.value), placeholder: "\u4F5C\u8005\u6635\u79F0" }), /* @__PURE__ */ React69__namespace.default.createElement("input", { value: title, onChange: (e) => setTitle(e.target.value), placeholder: "\u4F5C\u54C1\u6807\u9898" }), /* @__PURE__ */ React69__namespace.default.createElement("input", { value: desc, onChange: (e) => setDesc(e.target.value), placeholder: "\u4F5C\u54C1\u7B80\u4ECB" }), /* @__PURE__ */ React69__namespace.default.createElement("select", { value: type, onChange: (e) => setType(e.target.value) }, /* @__PURE__ */ React69__namespace.default.createElement("option", { value: "visual" }, "visual"), /* @__PURE__ */ React69__namespace.default.createElement("option", { value: "video" }, "video"), /* @__PURE__ */ React69__namespace.default.createElement("option", { value: "text" }, "text"), /* @__PURE__ */ React69__namespace.default.createElement("option", { value: "audio" }, "audio")), /* @__PURE__ */ React69__namespace.default.createElement("button", { onClick: submit }, "\u63D0\u4EA4")) : null);
10045
+ };
10046
+ var MikuContestMiniappPage_default = MikuContestMiniappPage;
10047
+
8802
10048
  // src/storage/adapters/react-native-adapter.ts
8803
10049
  var AsyncStorage = null;
8804
10050
  try {
@@ -9252,6 +10498,9 @@ exports.LocalImageMappingPanel = LocalImageMappingPanel;
9252
10498
  exports.LogLevel = LogLevel;
9253
10499
  exports.Logger = Logger;
9254
10500
  exports.MIKU_PALETTE = MIKU_PALETTE;
10501
+ exports.MikuContestPersistentService = MikuContestPersistentService;
10502
+ exports.MikuContestService = MikuContestService;
10503
+ exports.MikuContestStateDbService = MikuContestStateDbService;
9255
10504
  exports.MikuFireworks3D = MikuFireworks3D;
9256
10505
  exports.Modal = Modal;
9257
10506
  exports.NORMAL_PALETTE = NORMAL_PALETTE;
@@ -9316,14 +10565,30 @@ exports.applyPromptTemplate = applyPromptTemplate;
9316
10565
  exports.arrayUtils = arrayUtils;
9317
10566
  exports.badgeVariants = badgeVariants;
9318
10567
  exports.buttonVariants = buttonVariants;
10568
+ exports.checkVoteEligibility = checkVoteEligibility;
9319
10569
  exports.cn = cn;
9320
10570
  exports.createAiClient = createAiClient;
9321
10571
  exports.createChatSession = createChatSession;
10572
+ exports.createCreateSubmissionHandler = createCreateSubmissionHandler;
10573
+ exports.createDefaultMikuContestConfig = createDefaultMikuContestConfig;
10574
+ exports.createExportSubmissionsHandler = createExportSubmissionsHandler;
10575
+ exports.createGetContestSnapshotHandler = createGetContestSnapshotHandler;
9322
10576
  exports.createInMemoryFestivalCardDb = createInMemoryFestivalCardDb;
10577
+ exports.createListSubmissionsHandler = createListSubmissionsHandler;
9323
10578
  exports.createLogger = createLogger;
10579
+ exports.createMikuContestApiClient = createMikuContestApiClient;
10580
+ exports.createMikuContestDrizzlePersistenceAdapter = createMikuContestDrizzlePersistenceAdapter;
10581
+ exports.createMikuContestPersistentService = createMikuContestPersistentService;
10582
+ exports.createMikuContestService = createMikuContestService;
9324
10583
  exports.createOpenAICompatibleProvider = createOpenAICompatibleProvider;
10584
+ exports.createResetVotesHandler = createResetVotesHandler;
10585
+ exports.createReviewSubmissionHandler = createReviewSubmissionHandler;
10586
+ exports.createSetVoterRestrictionHandler = createSetVoterRestrictionHandler;
9325
10587
  exports.createSkillRegistry = createSkillRegistry;
10588
+ exports.createUpdateContestConfigHandler = createUpdateContestConfigHandler;
10589
+ exports.createVoteHandler = createVoteHandler;
9326
10590
  exports.debugUtils = debugUtils;
10591
+ exports.defaultMikuVotingRules = defaultMikuVotingRules;
9327
10592
  exports.defaultVocaloidBoothConfig = defaultVocaloidBoothConfig;
9328
10593
  exports.errorUtils = errorUtils;
9329
10594
  exports.fileUtils = fileUtils;
@@ -9339,6 +10604,14 @@ exports.getCompletionStatusText = getCompletionStatusText;
9339
10604
  exports.getExperimentCounts = getExperimentCounts;
9340
10605
  exports.japaneseUtils = japaneseUtils;
9341
10606
  exports.logger = logger;
10607
+ exports.mikuContestConfigs = mikuContestConfigs;
10608
+ exports.mikuContestDbService = mikuContestDbService;
10609
+ exports.mikuContestNotices = mikuContestNotices;
10610
+ exports.mikuContestSubmissions = mikuContestSubmissions;
10611
+ exports.mikuContestVoterRestrictions = mikuContestVoterRestrictions;
10612
+ exports.mikuContestVotes = mikuContestVotes;
10613
+ exports.miniappService = miniapp_exports;
10614
+ exports.miniappUI = miniapp_exports2;
9342
10615
  exports.normalizeFestivalCardConfig = normalizeFestivalCardConfig;
9343
10616
  exports.normalizeMatchCode = normalizeMatchCode;
9344
10617
  exports.normalizePromptVariables = normalizePromptVariables;
@@ -9346,8 +10619,10 @@ exports.normalizeVocaloidBoothConfig = normalizeVocaloidBoothConfig;
9346
10619
  exports.resizeFestivalCardPages = resizeFestivalCardPages;
9347
10620
  exports.resolveScreenReceiverSignalUrl = resolveScreenReceiverSignalUrl;
9348
10621
  exports.skillToToolDefinition = skillToToolDefinition;
10622
+ exports.sortByVotesDesc = sortByVotesDesc;
9349
10623
  exports.sortExperiments = sortExperiments;
9350
10624
  exports.stringUtils = stringUtils;
10625
+ exports.toVoteDayKey = toVoteDayKey;
9351
10626
  exports.useAiChat = useAiChat;
9352
10627
  exports.useAsyncStorage = useAsyncStorage;
9353
10628
  exports.useBackgroundRemoval = useBackgroundRemoval;
@@ -9357,6 +10632,7 @@ exports.useFestivalCardConfig = useFestivalCardConfig;
9357
10632
  exports.useFireworksEngine = useFireworksEngine;
9358
10633
  exports.useFireworksRealtime = useFireworksRealtime;
9359
10634
  exports.useLocalStorage = useLocalStorage;
10635
+ exports.useMikuContest = useMikuContest;
9360
10636
  exports.useOCR = useOCR;
9361
10637
  exports.useScreenReceiver = useScreenReceiver;
9362
10638
  exports.useSentimentAnalysis = useSentimentAnalysis;
@@ -9364,6 +10640,9 @@ exports.useStorage = useStorage;
9364
10640
  exports.useTaroStorage = useTaroStorage;
9365
10641
  exports.useTextGeneration = useTextGeneration;
9366
10642
  exports.validateExperiment = validateExperiment;
10643
+ exports.validateMikuSubmissionInput = validateMikuSubmissionInput;
9367
10644
  exports.validators = validators;
10645
+ exports.webService = web_exports;
10646
+ exports.webUI = web_exports2;
9368
10647
  //# sourceMappingURL=index.js.map
9369
10648
  //# sourceMappingURL=index.js.map