flyzto преди 1 месец
ревизия
76dd8c59d3
променени са 58 файла, в които са добавени 15111 реда и са изтрити 0 реда
  1. 46 0
      .gitignore
  2. 15 0
      pinnacle/betsapi_zh.html
  3. 46 0
      pinnacle/libs/cache.js
  4. 79 0
      pinnacle/libs/getDateInTimezone.js
  5. 45 0
      pinnacle/libs/logs.js
  6. 234 0
      pinnacle/libs/parseGameData.js
  7. 315 0
      pinnacle/libs/pinnacleClient.js
  8. 401 0
      pinnacle/linesapi_zh.html
  9. 762 0
      pinnacle/main.js
  10. 383 0
      pinnacle/package-lock.json
  11. 19 0
      pinnacle/package.json
  12. 46 0
      polymarket/libs/cache.js
  13. 79 0
      polymarket/libs/getDateInTimezone.js
  14. 45 0
      polymarket/libs/logs.js
  15. 310 0
      polymarket/libs/parseMarkets.js
  16. 272 0
      polymarket/libs/polymarketClient.js
  17. 229 0
      polymarket/libs/webSocketClient.js
  18. 357 0
      polymarket/main.js
  19. 1633 0
      polymarket/package-lock.json
  20. 21 0
      polymarket/package.json
  21. 46 0
      server/libs/cache.js
  22. 79 0
      server/libs/getDateInTimezone.js
  23. 27 0
      server/libs/getTranslation.js
  24. 45 0
      server/libs/logs.js
  25. 84 0
      server/libs/processData.js
  26. 127 0
      server/libs/wsClientData.js
  27. 84 0
      server/main.js
  28. 221 0
      server/models/Games.js
  29. 15 0
      server/models/Locales.js
  30. 446 0
      server/models/Markets.js
  31. 176 0
      server/models/Platforms.js
  32. 41 0
      server/models/Translation.js
  33. 2336 0
      server/package-lock.json
  34. 23 0
      server/package.json
  35. 120 0
      server/routes/games.js
  36. 38 0
      server/routes/locales.js
  37. 60 0
      server/routes/platforms.js
  38. 112 0
      server/state/locales.js
  39. 147 0
      server/state/store.js
  40. 24 0
      server/state/wsclients.js
  41. 209 0
      server/triangle/eventSolutions.js
  42. 370 0
      server/triangle/iorKeys.js
  43. 39 0
      server/triangle/main.js
  44. 156 0
      server/triangle/trangleCalc.js
  45. 38 0
      web/README.md
  46. 13 0
      web/index.html
  47. 8 0
      web/jsconfig.json
  48. 3550 0
      web/package-lock.json
  49. 27 0
      web/package.json
  50. BIN
      web/public/favicon.ico
  51. 11 0
      web/src/main.js
  52. 57 0
      web/src/main.vue
  53. 37 0
      web/src/router/index.js
  54. 364 0
      web/src/views/games.vue
  55. 171 0
      web/src/views/home.vue
  56. 314 0
      web/src/views/leagues.vue
  57. 133 0
      web/src/views/locales.vue
  58. 26 0
      web/vite.config.js

+ 46 - 0
.gitignore

@@ -0,0 +1,46 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+coverage
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Mac specific files
+.DS_Store
+
+# Python related (for your spider directory)
+__pycache__/
+*.py[cod]
+*$py.class
+
+venv/
+env/
+*.so
+.env
+
+# Build artifacts
+dist/
+build/
+
+data/
+cache/
+
+.credentials.json

Файловите разлики са ограничени, защото са твърде много
+ 15 - 0
pinnacle/betsapi_zh.html


+ 46 - 0
pinnacle/libs/cache.js

@@ -0,0 +1,46 @@
+import fs from 'fs';
+import path from 'path';
+
+export const getData = (file) => {
+  let data = null;
+
+  if (fs.existsSync(file)) {
+    const arrayBuffer = fs.readFileSync(file);
+
+    try {
+      data = JSON.parse(arrayBuffer.toString());
+    }
+    catch (e) {}
+  }
+
+  return Promise.resolve(data);
+}
+
+export const setData = (file, data, indent = 2) => {
+  return new Promise((resolve, reject) => {
+
+    if (typeof (data) != 'string') {
+      try {
+        data = JSON.stringify(data, null, indent);
+      }
+      catch (error) {
+        reject(error);
+      }
+    }
+
+    const directoryPath = path.dirname(file);
+    if(!fs.existsSync(directoryPath)) {
+      fs.mkdirSync(directoryPath, { recursive: true });
+    }
+
+    try {
+      fs.writeFileSync(file, data);
+      resolve();
+    }
+    catch (error) {
+      reject(error);
+    }
+  });
+}
+
+export default { getData, setData };

+ 79 - 0
pinnacle/libs/getDateInTimezone.js

@@ -0,0 +1,79 @@
+/**
+ * 将时区偏移小时转换为 ±HHMM 格式
+ * @param {number|string} offset 如 8, -4, "+8", "-04"
+ * @returns {string} 如 "+0800", "-0400"
+ */
+const formatTimezoneOffset = (offset) => {
+  const hours = Number(offset);
+  if (Number.isNaN(hours)) {
+    throw new Error('Invalid timezone offset');
+  }
+
+  const sign = hours >= 0 ? '+' : '-';
+  const hh = String(Math.abs(hours)).padStart(2, '0');
+
+  return `${sign}${hh}00`;
+}
+
+
+/**
+ * 判断日期是否无效
+ * @param {Date} date
+ * @returns {boolean}
+ */
+const isInvalidDate = (date) => {
+  return date instanceof Date && Number.isNaN(date.getTime());
+}
+
+
+/**
+ * 获取指定时区当前日期或时间
+ * @param {number} offset - 时区相对 UTC 的偏移(例如:-4 表示 GMT-4)
+ * @param {number} timestamp - 时间戳(可选)
+ * @param {boolean} [withTime=false] - 是否返回完整时间(默认只返回日期)
+ * @returns {string} 格式化的日期或时间字符串
+ */
+const getDateInTimezone = (offset, timestamp, withTime=false) => {
+
+  offset = Number(offset);
+  if (Number.isNaN(offset)) {
+    throw new Error('Invalid timezone offset');
+  }
+
+  if (typeof(timestamp) === 'undefined') {
+    timestamp = Date.now();
+  }
+  else if (typeof(timestamp) === 'boolean') {
+    withTime = timestamp;
+    timestamp = Date.now();
+  }
+  else if (typeof(timestamp) === 'string') {
+    const date = new Date(timestamp);
+    if (isInvalidDate(date)) {
+      throw new Error('Invalid timestamp');
+    }
+    timestamp = date.getTime();
+  }
+  else if (typeof(timestamp) !== 'number') {
+    throw new Error('Invalid timestamp');
+  }
+
+  const nowUTC = new Date(timestamp);
+  const targetTime = new Date(nowUTC.getTime() + offset * 60 * 60 * 1000);
+
+  const year = targetTime.getUTCFullYear();
+  const month = String(targetTime.getUTCMonth() + 1).padStart(2, '0');
+  const day = String(targetTime.getUTCDate()).padStart(2, '0');
+
+  if (!withTime) {
+    return `${year}-${month}-${day}`;
+  }
+
+  const hours = String(targetTime.getUTCHours()).padStart(2, '0');
+  const minutes = String(targetTime.getUTCMinutes()).padStart(2, '0');
+  const seconds = String(targetTime.getUTCSeconds()).padStart(2, '0');
+
+  return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${formatTimezoneOffset(offset)}`;
+}
+
+export default getDateInTimezone;

+ 45 - 0
pinnacle/libs/logs.js

@@ -0,0 +1,45 @@
+import dayjs from 'dayjs';
+
+export default class Logs {
+
+  static out(...args) {
+    const timeString = dayjs().format('YYYY-MM-DD HH:mm:ss.SSS');
+    if (typeof args[0] === 'string' && args[0].includes('%')) {
+      args[0] = `[${timeString}] ` + args[0];
+    }
+    else {
+      args.unshift(`[${timeString}]`);
+    }
+    console.log(...args);
+  }
+
+  static err(...args) {
+    const timeString = dayjs().format('YYYY-MM-DD HH:mm:ss.SSS');
+    if (typeof args[0] === 'string' && args[0].includes('%')) {
+      args[0] = `[${timeString}] ` + args[0];
+    }
+    else {
+      args.unshift(`[${timeString}]`);
+    }
+    console.error(...args);
+  }
+
+  static outDev(...args) {
+    if (process.env.NODE_ENV == 'development') {
+      this.out(...args);
+    }
+  }
+
+  static errDev(...args) {
+    if (process.env.NODE_ENV == 'development') {
+      this.err(...args);
+    }
+  }
+
+  static outLine(string) {
+    process.stdout.write("\u001b[1A");
+    process.stdout.write("\u001b[2K");
+    this.out(string);
+  }
+
+}

+ 234 - 0
pinnacle/libs/parseGameData.js

@@ -0,0 +1,234 @@
+/**
+ * 解析让球方
+ */
+const ratioAccept = (ratio) => {
+  if (ratio > 0) {
+    return 'a';
+  }
+  return '';
+}
+
+/**
+ * 将比率转换为字符串
+ * @param {*} ratio
+ * @returns
+ */
+const ratioString = (ratio) => {
+  ratio = Math.abs(ratio);
+  ratio = ratio.toString();
+  ratio = ratio.replace(/\./, '');
+  return ratio;
+}
+
+/**
+ * 解析胜平负盘口
+ * @param {*} moneyline
+ * @returns
+ */
+const parseMoneyline = (moneyline, maxMoneyline) => {
+  // 胜平负
+  if (!moneyline) {
+    return null;
+  }
+  const { home, away, draw } = moneyline;
+  const max = maxMoneyline;
+  return {
+    'ior_mh': { v: home, m: max },
+    'ior_mc': { v: away, m: max },
+    'ior_mn': { v: draw, m: max },
+  }
+}
+
+/**
+ * 解析让分盘口
+ * @param {*} spreads
+ * @param {*} wm
+ * @returns
+ */
+const parseSpreads = (spreads, wm, maxSpread) => {
+  // 让分盘
+  if (!spreads?.length) {
+    return null;
+  }
+  const events = {};
+  spreads.forEach(spread => {
+    const { hdp, home, away, max=maxSpread } = spread;
+
+    // if (!(hdp % 1) || !!(hdp % 0.5)) {
+    //   // 整数或不能被0.5整除的让分盘不处理
+    //   return;
+    // }
+
+    const ratio_ro = hdp;
+    const ratio_r = ratio_ro - wm;
+    events[`ior_r${ratioAccept(ratio_r)}h_${ratioString(ratio_r)}`] = {
+      v: home,
+      r: wm != 0 ? `ior_r${ratioAccept(ratio_ro)}h_${ratioString(ratio_ro)}` : undefined,
+      m: max,
+    };
+    events[`ior_r${ratioAccept(-ratio_r)}c_${ratioString(ratio_r)}`] = {
+      v: away,
+      r: wm != 0 ? `ior_r${ratioAccept(-ratio_ro)}c_${ratioString(ratio_ro)}` : undefined,
+      m: max,
+    };
+  });
+  return events;
+}
+
+/**
+ * 解析大小球盘口
+ * @param {*} totals
+ * @returns
+ */
+const parseTotals = (totals, maxTotal) => {
+  // 大小球盘
+  if (!totals?.length) {
+    return null;
+  }
+  const events = {};
+
+  totals.forEach(total => {
+    const { points, over, under, max=maxTotal } = total;
+    events[`ior_ouc_${ratioString(points)}`] = { v: over, m: max };
+    events[`ior_ouh_${ratioString(points)}`] = { v: under, m: max };
+  });
+  return events;
+}
+
+/**
+ * 解析直赛盘口
+ * @param {*} straight
+ * @param {*} wm
+ * @returns
+ */
+const parseStraight = (straight, wm) => {
+  if (!straight) {
+    return null;
+  }
+  const { cutoff='', status=0, spreads=[], moneyline={}, totals=[], maxSpread, maxMoneyline, maxTotal } = straight;
+  const cutoffTime = new Date(cutoff).getTime();
+  const nowTime = Date.now();
+
+  if (status != 1 || cutoffTime < nowTime) {
+    return null;
+  }
+
+  const events = {};
+  Object.assign(events, parseSpreads(spreads, wm, maxSpread));
+  Object.assign(events, parseMoneyline(moneyline, maxMoneyline));
+  Object.assign(events, parseTotals(totals, maxTotal));
+
+  return events;
+}
+
+/**
+ * 解析净胜球盘口
+ * @param {*} winningMargin
+ * @param {*} home
+ * @param {*} away
+ * @returns
+ */
+const parseWinningMargin = (winningMargin, home, away) => {
+  if (!winningMargin) {
+    return null;
+  }
+  const { cutoff='', status='', contestants=[] } = winningMargin;
+  const cutoffTime = new Date(cutoff).getTime();
+  const nowTime = Date.now();
+
+  if (status != 'O' || cutoffTime < nowTime || !contestants?.length) {
+    return null;
+  }
+
+  const events = {};
+  contestants.forEach(contestant => {
+    const { name, price, max } = contestant;
+    const nr = name.match(/\d+$/)?.[0];
+    if (!nr) {
+      return;
+    }
+    let side;
+    if (name.startsWith(home)) {
+      side = 'h';
+    }
+    else if (name.startsWith(away)) {
+      side = 'c';
+    }
+    else {
+      return;
+    }
+    events[`ior_wm${side}_${nr}`] = { v: price, m: max };
+  });
+  return events;
+}
+
+/**
+ * 解析进球数盘口
+ * @param {*} exactTotalGoals
+ * @returns
+ */
+const parseExactTotalGoals = (exactTotalGoals) => {
+  if (!exactTotalGoals) {
+    return null;
+  }
+  const { cutoff='', status='', contestants=[] } = exactTotalGoals;
+  const cutoffTime = new Date(cutoff).getTime();
+  const nowTime = Date.now();
+
+  if (status != 'O' || cutoffTime < nowTime || !contestants?.length) {
+    return null;
+  }
+
+  const events = {};
+  contestants.forEach(contestant => {
+    const { name, price, max } = contestant;
+    if (+name >= 1 && +name <= 7) {
+      events[`ior_ot_${name}`] = { v: price, m: max };
+    }
+  });
+  return events;
+}
+
+/**
+ * 解析滚球场次状态
+ * @param {*} state
+ * @returns
+ */
+const parseRbState = (state) => {
+  let stage = null;
+  if (state == 1) {
+    stage = '1H';
+  }
+  else if (state == 2) {
+    stage = 'HT';
+  }
+  else if (state == 3) {
+    stage = '2H';
+  }
+  else if (state >= 4) {
+    stage = 'ET';
+  }
+  return stage;
+}
+
+/**
+ * 解析比赛数据
+ * @param {*} game
+ * @returns
+ */
+const parseGame = (game) => {
+  const { id=0, originId=0, periods={}, specials={}, home, away, marketType, state, elapsed, homeScore=0, awayScore=0 } = game;
+  const { straight } = periods;
+  const { winningMargin={}, exactTotalGoals={} } = specials;
+  const wm = homeScore - awayScore;
+  const score = `${homeScore}-${awayScore}`;
+  const odds = parseStraight(straight, wm) ?? {};
+  const stage = parseRbState(state);
+  const retime = elapsed ? `${elapsed}'` : '';
+  const evtime = Date.now();
+  Object.assign(odds, parseWinningMargin(winningMargin, home, away));
+  Object.assign(odds, parseExactTotalGoals(exactTotalGoals));
+  return { id, originId, odds, evtime, stage, retime, score, wm, marketType }
+}
+
+export default parseGame;

+ 315 - 0
pinnacle/libs/pinnacleClient.js

@@ -0,0 +1,315 @@
+import dotenv from 'dotenv';
+import axios from "axios";
+import { HttpsProxyAgent } from "https-proxy-agent";
+
+import { randomUUID } from 'crypto';
+
+import Logs from "./logs.js";
+import getDateInTimezone from "./getDateInTimezone.js";
+
+dotenv.config();
+
+const axiosDefaultOptions = {
+  baseURL: "",
+  url: "",
+  method: "GET",
+  headers: {},
+  params: {},
+  data: {},
+  timeout: 10000,
+};
+
+const pinnacleWebOptions = {
+  ...axiosDefaultOptions,
+  baseURL: "https://www.part987.com",
+  headers: {
+    "accept": "application/json, text/plain, */*",
+    "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
+    "x-app-data": "directusToken=TwEdnphtyxsfMpXoJkCkWaPsL2KJJ3lo;lang=zh_CN;dpVXz=ZDfaFZUP9"
+  },
+}
+
+export const pinnacleRequest = async (options) => {
+
+  const { url, username, password, ...optionsRest } = options;
+  if (!url || !username || !password) {
+    throw new Error("url、username、password is required");
+  }
+
+  const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
+  const axiosConfig = { ...axiosDefaultOptions, ...optionsRest, url, baseURL: "https://api.pinnacle888.com" };
+  Object.assign(axiosConfig.headers, {
+    "Authorization": authHeader,
+    "Accept": "application/json",
+  });
+  const proxy = process.env.NODE_HTTP_PROXY;
+  if (proxy) {
+    axiosConfig.proxy = false;
+    axiosConfig.httpsAgent = new HttpsProxyAgent(proxy);
+  }
+  return axios(axiosConfig).then(res => {
+    // Logs.out('pinnacle request', url, axiosConfig, res.data);
+    return res.data;
+  });
+}
+
+/**
+ * Pinnacle API Get请求
+ * @param {*} url
+ * @param {*} params
+ * @returns
+ */
+export const pinnacleGet = async (url, params) => {
+  return pinnacleRequest({
+    url,
+    params,
+    username: process.env.PINNACLE_USERNAME,
+    password: process.env.PINNACLE_PASSWORD,
+  })
+  .catch(err => {
+    const source = { url, params };
+    if (err?.response?.data) {
+      const data = err.response.data;
+      Object.assign(source, { data });
+    }
+    err.source = source;
+    return Promise.reject(err);
+  });
+}
+
+/**
+ * Pinnacle API Post请求
+ * @param {*} url
+ * @param {*} data
+ * @returns
+ */
+export const pinnaclePost = async (url, data) => {
+  return pinnacleRequest({
+    url,
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data,
+    username: process.env.PINNACLE_USERNAME,
+    password: process.env.PINNACLE_PASSWORD,
+  });
+}
+
+/**
+ * 清理对象中的undefined值
+ * @param {*} obj
+ * @returns {Object}
+ */
+const cleanUndefined = (obj) => {
+  return Object.fromEntries(
+    Object.entries(obj).filter(([, v]) => v !== undefined)
+  );
+}
+
+/**
+ * 获取直盘线
+ */
+export const getLineInfo = async (info = {}) => {
+  const {
+    leagueId, eventId, betType, handicap, team, side,
+    specialId, contestantId,
+    periodNumber=0, oddsFormat='Decimal', sportId=29
+  } = info;
+  let url = '/v2/line/';
+  let data = { sportId, leagueId, eventId, betType, handicap, periodNumber, team, side, oddsFormat };
+  if (specialId) {
+    url = '/v2/line/special';
+    data = { specialId, contestantId, oddsFormat };
+  }
+  data = cleanUndefined(data);
+  return pinnacleGet(url, data)
+  .then(ret => ({ info: ret, line: data }))
+  .catch(err => {
+    Logs.errDev('get line info error', err);
+    return Promise.reject(err);
+  });
+}
+
+/**
+ * 获取账户余额
+ */
+export const getAccountBalance = async () => {
+  return pinnacleGet('/v1/client/balance');
+}
+
+/**
+ * 下注
+ */
+export const placeOrder = async ({ info, line, stakeSize }) => {
+  // return Promise.resolve({info, line, stakeSize});
+  const uuid = randomUUID()
+  if (line.specialId) {
+    const data = cleanUndefined({
+      oddsFormat: line.oddsFormat,
+      uniqueRequestId: uuid,
+      acceptBetterLine: true,
+      stake: stakeSize,
+      winRiskStake: 'RISK',
+      lineId: info.lineId,
+      specialId: info.specialId,
+      contestantId: info.contestantId,
+    });
+    Logs.outDev('pinnacle place order data', data);
+    return pinnaclePost('/v4/bets/special', { bets: [data] })
+    .then(ret => ret.bets?.[0] ?? ret)
+    .then(ret => {
+      Logs.outDev('pinnacle place order', ret, uuid);
+      return ret;
+    })
+    .catch(err => {
+      Logs.outDev('pinnacle place order error', err.response.data, uuid);
+      Logs.errDev(err);
+      return Promise.reject(err);
+    });
+  }
+  else {
+    const data = cleanUndefined({
+      oddsFormat: line.oddsFormat,
+      uniqueRequestId: uuid,
+      acceptBetterLine: true,
+      stake: stakeSize,
+      winRiskStake: 'RISK',
+      lineId: info.lineId,
+      altLineId: info.altLineId,
+      fillType: 'NORMAL',
+      sportId: line.sportId,
+      eventId: line.eventId,
+      periodNumber: line.periodNumber,
+      betType: line.betType,
+      team: line.team,
+      side: line.side,
+      handicap: line.handicap,
+    });
+    Logs.outDev('pinnacle place order data', data);
+    return pinnaclePost('/v4/bets/place', data)
+    .then(ret => {
+      Logs.outDev('pinnacle place order', ret, uuid);
+      return ret;
+    })
+    .catch(err => {
+      Logs.outDev('pinnacle place order error', err.response.data, uuid);
+      Logs.errDev(err);
+      return Promise.reject(err);
+    });
+  }
+}
+
+/**
+ * 获取Web端联赛数据
+ * @param {*} marketType 0: 早盘赛事, 1: 今日赛事
+ * @param {*} locale en_US or zh_CN
+ * @returns
+ */
+export const pinnacleWebLeagues = async (marketType = 1, locale = "zh_CN") => {
+  const dateString = marketType == 1 ? getDateInTimezone(-4) : getDateInTimezone(-4, Date.now()+24*60*60*1000);
+  const nowTime = Date.now();
+  const axiosConfig = {
+    ...pinnacleWebOptions,
+    url: "/sports-service/sv/compact/leagues",
+    params: {
+      btg: 1, c: "", d: dateString,
+      l: true,  mk: marketType,
+      pa: 0, pn: -1, sp: 29, tm: 0,
+      locale, _: nowTime, withCredentials: true
+    },
+  };
+  const proxy = process.env.NODE_HTTP_PROXY;
+  if (proxy) {
+    axiosConfig.proxy = false;
+    axiosConfig.httpsAgent = new HttpsProxyAgent(proxy);
+  }
+  return axios(axiosConfig).then(res => res.data?.[0]?.[2]?.map(item => {
+    const [ id, , name ] = item;
+    return [id, { id, name }];
+  }) ?? []);
+}
+
+/**
+ * 获取Web比赛列表
+ */
+export const pinnacleWebGames = async (leagues=[], marketType=1, locale="zh_CN") => {
+  const dateString = marketType == 1 ? getDateInTimezone(-4) : getDateInTimezone(-4, Date.now()+24*60*60*1000);
+  const nowTime = Date.now();
+  const axiosConfig = {
+    ...pinnacleWebOptions,
+    url: "/sports-service/sv/odds/events",
+    params: {
+      sp: 29, lg: leagues.join(','), ev: "",
+      mk: marketType, btg: 1, ot: 1,
+      d: dateString, o: 0, l: 100, v: 0,
+      me: 0, more: false, tm: 0, pa: 0, c: "",
+      g: "QQ==", cl: 100, pimo: "0,1,8,39,2,3,6,7,4,5",
+      inl: false, _: nowTime, locale
+    },
+  };
+  const proxy = process.env.NODE_HTTP_PROXY;
+  if (proxy) {
+    axiosConfig.proxy = false;
+    axiosConfig.httpsAgent = new HttpsProxyAgent(proxy);
+  }
+  return axios(axiosConfig).then(res => res.data.n?.[0]?.[2]?.map(league => {
+    const [leagueId, leagueName, games] = league;
+    return games.map(game => {
+      const [id, teamHomeName, teamAwayName, , timestamp] = game;
+      const startTime = getDateInTimezone('+8', timestamp, true);
+      return { id, leagueId, leagueName, teamHomeName, teamAwayName, timestamp, startTime }
+    })
+  }).flat().filter(game => {
+    const { teamHomeName, teamAwayName } = game;
+    if (teamHomeName.startsWith('主队')) {
+      return false;
+    }
+    else if (teamAwayName.startsWith('客队')) {
+      return false;
+    }
+    else if (teamHomeName.startsWith('Home Teams')) {
+      return false;
+    }
+    else if (teamAwayName.startsWith('Away Teams')) {
+      return false;
+    }
+    return true;
+  }) ?? []);
+}
+
+export const platformRequest = async (options) => {
+  const { url } = options;
+  if (!url) {
+    throw new Error("url is required");
+  }
+  const mergedOptions = {
+    ...axiosDefaultOptions,
+    ...options,
+    baseURL: "http://127.0.0.1:9020",
+  };
+  return axios(mergedOptions).then(res => res.data);
+}
+
+export const platformPost = async (url, data) => {
+  return platformRequest({
+    url,
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data,
+  });
+}
+
+export const platformGet = async (url, params) => {
+  return platformRequest({ url, method: 'GET', params });
+}
+
+/**
+ * 通知异常
+ * @param {*} message
+ */
+export const notifyException = async (message) => {
+  Logs.out('notify exception', message);
+}

Файловите разлики са ограничени, защото са твърде много
+ 401 - 0
pinnacle/linesapi_zh.html


+ 762 - 0
pinnacle/main.js

@@ -0,0 +1,762 @@
+import path from "path";
+import { fileURLToPath } from "url";
+
+import Logs from "./libs/logs.js";
+import { getData, setData } from "./libs/cache.js";
+import getDateInTimezone from "./libs/getDateInTimezone.js";
+
+import {
+  pinnacleGet,
+  pinnacleWebLeagues, pinnacleWebGames,
+  platformPost, platformGet,
+  notifyException,
+} from "./libs/pinnacleClient.js";
+import parseGame from "./libs/parseGameData.js";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const gamesMapCacheFile = path.join(__dirname, './cache/pinnacleGamesCache.json');
+const globalDataCacheFile = path.join(__dirname, './cache/pinnacleGlobalDataCache.json');
+
+const GLOBAL_DATA = {
+  filteredLeagues: [],
+  filteredGames: [],
+  gamesMap: {},
+  straightFixturesVersion: 0,
+  straightFixturesCount: 0,
+  specialFixturesVersion: 0,
+  specialFixturesCount: 0,
+  straightOddsVersion: 0,
+  // straightOddsCount: 0,
+  specialsOddsVersion: 0,
+  // specialsOddsCount: 0,
+  requestErrorCount: 0,
+  loopActive: false,
+  loopLastResultTime: 0,
+  wsClientData: null,
+};
+
+const resetVersionsCount = () => {
+  GLOBAL_DATA.straightFixturesVersion = 0;
+  GLOBAL_DATA.specialFixturesVersion = 0;
+  GLOBAL_DATA.straightOddsVersion = 0;
+  GLOBAL_DATA.specialsOddsVersion = 0;
+
+  GLOBAL_DATA.straightFixturesCount = 0;
+  GLOBAL_DATA.specialFixturesCount = 0;
+}
+
+const incrementVersionsCount = () => {
+  GLOBAL_DATA.straightFixturesCount += 1;
+  GLOBAL_DATA.specialFixturesCount += 1;
+}
+
+/**
+ * 更新Web联赛列表
+ */
+const updateWebLeagues = async () => {
+  const getWebLeaguesToday = pinnacleWebLeagues(1);
+  const getWebLeaguesTomorrow = pinnacleWebLeagues(0);
+  return Promise.all([getWebLeaguesToday, getWebLeaguesTomorrow])
+  .then(data => {
+    const [webLeaguesToday, webLeaguesTomorrow] = data;
+    const leaguesMap = new Map(webLeaguesToday.concat(webLeaguesTomorrow));
+    return Array.from(leaguesMap.values()).sort((a, b) => a.id - b.id);
+  })
+  .then(leagues => {
+    Logs.outDev('update leagues list', leagues);
+    return platformPost('/api/platforms/update_leagues', { platform: 'pinnacle', leagues });
+  })
+  .then(() => {
+    Logs.outDev('leagues list updated');
+    return Promise.resolve(60);
+  });
+}
+
+/**
+ * 定时更新Web联赛列表
+ */
+const updateWebLeaguesLoop = () => {
+  updateWebLeagues()
+  .then(delay => {
+    setTimeout(() => {
+      updateWebLeaguesLoop();
+    }, 1000 * delay);
+  })
+  .catch(err => {
+    Logs.err('failed to update leagues list', err.message);
+    setTimeout(() => {
+      updateWebLeaguesLoop();
+    }, 1000 * 5);
+  });
+}
+
+/**
+ * 更新Web比赛列表
+ */
+const updateWebGames = async () => {
+  if (!GLOBAL_DATA.filteredLeagues.length) {
+    return Promise.resolve(5);
+  }
+  const getWebGamesToday = pinnacleWebGames(GLOBAL_DATA.filteredLeagues, 1);
+  const getWebGamesTomorrow = pinnacleWebGames(GLOBAL_DATA.filteredLeagues, 0);
+  return Promise.all([getWebGamesToday, getWebGamesTomorrow])
+  .then(data => {
+    const [webGamesToday, webGamesTomorrow] = data;
+    return webGamesToday.concat(webGamesTomorrow).sort((a, b) => a.timestamp - b.timestamp);
+  })
+  .then(games => {
+    Logs.outDev('update games list', games);
+    return platformPost('/api/platforms/update_games', { platform: 'pinnacle', games });
+  })
+  .then(() => {
+    Logs.outDev('games list updated');
+    return Promise.resolve(60);
+  });
+}
+
+/**
+ * 定时更新Web比赛列表
+ */
+const updateWebGamesLoop = () => {
+  updateWebGames()
+  .then(delay => {
+    setTimeout(() => {
+      updateWebGamesLoop();
+    }, 1000 * delay);
+  })
+  .catch(err => {
+    Logs.err('failed to update games list', err.message);
+    setTimeout(() => {
+      updateWebGamesLoop();
+    }, 1000 * 5);
+  });
+}
+
+/**
+ * 获取过滤后的联赛数据
+ * @returns
+ */
+const updateFilteredLeagues = () => {
+  platformGet('/api/platforms/get_filtered_leagues', { platform: 'pinnacle' })
+  .then(res => {
+    const { data: filteredLeagues } = res;
+    GLOBAL_DATA.filteredLeagues = filteredLeagues.map(item => item.id);
+  })
+  .catch(error => {
+    Logs.err('failed to update filtered leagues', error.message);
+  })
+  .finally(() => {
+    setTimeout(() => {
+      updateFilteredLeagues();
+    }, 1000 * 10);
+  });
+}
+
+/**
+ * 获取过滤后的比赛数据
+ * @returns
+ */
+const updateFilteredGames = () => {
+  platformGet('/api/platforms/get_filtered_games', { platform: 'pinnacle' })
+  .then(res => {
+    const { data: filteredGames } = res;
+    GLOBAL_DATA.filteredGames = filteredGames.map(item => item.id);
+  })
+  .catch(error => {
+    Logs.err('failed to update filtered games', error.message);
+  })
+  .finally(() => {
+    setTimeout(() => {
+      updateFilteredGames();
+    }, 1000 * 10);
+  });
+}
+
+/**
+ * 获取直赛数据
+ * @returns
+ */
+const getStraightFixtures = async () => {
+  if (!GLOBAL_DATA.filteredLeagues.length) {
+    resetVersionsCount();
+    return Promise.reject(new Error('no filtered leagues', { cause: 400 }));
+  }
+  const leagueIds = GLOBAL_DATA.filteredLeagues.join(',');
+  let since = GLOBAL_DATA.straightFixturesVersion;
+  if (GLOBAL_DATA.straightFixturesCount >= 12) {
+    since = 0;
+    GLOBAL_DATA.straightFixturesCount = 0;
+  }
+  if (since == 0) {
+    Logs.outDev('full update straight fixtures');
+  }
+  return pinnacleGet('/v3/fixtures', { sportId: 29, leagueIds, since })
+  .then(data => {
+    const { league, last } = data;
+    if (!last) {
+      return {};
+    }
+    GLOBAL_DATA.straightFixturesVersion = last;
+    const games = league?.map(league => {
+      const { id: leagueId, events } = league;
+      return events?.map(event => {
+        const { starts } = event;
+        const timestamp = new Date(starts).getTime();
+        return { leagueId, ...event, timestamp };
+      });
+    })
+    .flat() ?? [];
+    const update = since == 0 ? 'full' : 'increment';
+    return { games, update };
+  });
+}
+
+/**
+ * 更新直赛数据
+ * @returns
+ */
+const updateStraightFixtures = async () => {
+  return getStraightFixtures()
+  .then(data => {
+    const { games, update } = data ?? {};
+    const { gamesMap } = GLOBAL_DATA;
+    if (games?.length) {
+      games.forEach(game => {
+        const { id } = game;
+        if (!gamesMap[id]) {
+          gamesMap[id] = game;
+        }
+        else {
+          Object.assign(gamesMap[id], game);
+        }
+      });
+    }
+    if (update && update == 'full') {
+      const gamesSet = new Set(games.map(game => game.id));
+      Object.keys(gamesMap).forEach(key => {
+        if (!gamesSet.has(+key)) {
+          delete gamesMap[key];
+        }
+      });
+    }
+    return Promise.resolve('StraightFixtures');
+  });
+}
+
+/**
+ * 获取直赛赔率数据
+ * @returns
+ */
+const getStraightOdds = async () => {
+  if (!GLOBAL_DATA.filteredLeagues.length) {
+    resetVersionsCount();
+    return Promise.reject(new Error('no filtered leagues', { cause: 400 }));
+  }
+  const leagueIds = GLOBAL_DATA.filteredLeagues.join(',');
+  const since = GLOBAL_DATA.straightOddsVersion;
+  // if (GLOBAL_DATA.straightOddsCount >= 27) {
+  //   since = 0;
+  //   GLOBAL_DATA.straightOddsCount = 3;
+  // }
+  if (since == 0) {
+    Logs.outDev('full update straight odds');
+  }
+  return pinnacleGet('/v3/odds', { sportId: 29, oddsFormat: 'Decimal', leagueIds, since })
+  .then(data => {
+    const { leagues, last } = data;
+    if (!last) {
+      return [];
+    }
+    GLOBAL_DATA.straightOddsVersion = last;
+    const games = leagues?.flatMap(league => league.events);
+    // return games;
+    return games?.map(item => {
+      const { periods, ...rest } = item;
+      const straight = periods?.find(period => period.number == 0);
+      return { ...rest, periods: { straight }};
+    }) ?? [];
+  });
+}
+
+/**
+ * 更新直赛赔率数据
+ * @returns
+ */
+const updateStraightOdds = async () => {
+  return getStraightOdds()
+  .then(games => {
+    if (games?.length) {
+      const { gamesMap } = GLOBAL_DATA;
+      games.forEach(game => {
+        const { id, periods, ...rest } = game;
+        const localGame = gamesMap[id];
+        if (localGame) {
+          Object.assign(localGame, rest);
+          if (!localGame.periods && periods) {
+            localGame.periods = periods;
+          }
+          else if (periods) {
+            Object.assign(localGame.periods, periods);
+          }
+        }
+      });
+    }
+    return Promise.resolve('StraightOdds');
+  });
+}
+
+/**
+ * 获取特殊赛数据
+ * @returns
+ */
+const getSpecialFixtures = async () => {
+  if (!GLOBAL_DATA.filteredLeagues.length) {
+    resetVersionsCount();
+    return Promise.reject(new Error('no filtered leagues', { cause: 400 }));
+  }
+  const leagueIds = GLOBAL_DATA.filteredLeagues.join(',');
+  let since = GLOBAL_DATA.specialFixturesVersion;
+  if (GLOBAL_DATA.specialFixturesCount >= 18) {
+    since = 0;
+    GLOBAL_DATA.specialFixturesCount = 6;
+  }
+  if (since == 0) {
+    Logs.outDev('full update special fixtures');
+  }
+  return pinnacleGet('/v2/fixtures/special', { sportId: 29, leagueIds, since })
+  .then(data => {
+    const { leagues, last } = data;
+    if (!last) {
+      return [];
+    }
+    GLOBAL_DATA.specialFixturesVersion = last;
+    const specials = leagues?.map(league => {
+      const { specials } = league;
+      return specials?.filter(special => special.event)
+      .map(special => {
+        const { event: { id: eventId, periodNumber }, ...rest } = special ?? { event: {} };
+        return { eventId, periodNumber, ...rest };
+      }) ?? [];
+    })
+    .flat()
+    .filter(special => {
+      if (special.name.includes('Winning Margin') || special.name.includes('Exact Total Goals')) {
+        return true;
+      }
+      return false;
+    }) ?? [];
+    const update = since == 0 ? 'full' : 'increment';
+    return { specials, update };
+  });
+}
+
+/**
+ * 合并特殊赛盘口数据
+ * @param {*} localContestants
+ * @param {*} remoteContestants
+ * @returns
+ */
+const mergeContestants = (localContestants=[], remoteContestants=[]) => {
+  const localContestantsMap = new Map(localContestants.map(contestant => [contestant.id, contestant]));
+  remoteContestants.forEach(contestant => {
+    const localContestant = localContestantsMap.get(contestant.id);
+    if (localContestant) {
+      Object.assign(localContestant, contestant);
+    }
+    else {
+      localContestants.push(contestant);
+    }
+  });
+  const remoteContestantsMap = new Map(remoteContestants.map(contestant => [contestant.id, contestant]));
+  for (let i = localContestants.length - 1; i >= 0; i--) {
+    if (!remoteContestantsMap.has(localContestants[i].id)) {
+      localContestants.splice(i, 1);
+    }
+  }
+  return localContestants;
+}
+
+/**
+ * 合并特殊赛数据
+ * @param {*} localSpecials
+ * @param {*} remoteSpecials
+ * @param {*} specialName
+ * @returns
+ */
+const mergeSpecials = (localSpecials, remoteSpecials, specialName) => {
+  if (localSpecials[specialName] && remoteSpecials[specialName]) {
+    const { contestants: specialContestants, ...specialRest } = remoteSpecials[specialName];
+    Object.assign(localSpecials[specialName], specialRest);
+    mergeContestants(localSpecials[specialName].contestants, specialContestants);
+  }
+  else if (remoteSpecials[specialName]) {
+    localSpecials[specialName] = remoteSpecials[specialName];
+  }
+}
+
+/**
+ * 更新特殊赛数据
+ * @returns
+ */
+const updateSpecialFixtures = async () => {
+  return getSpecialFixtures()
+  .then(data => {
+    const { specials, update } = data ?? {};
+    if (specials?.length) {
+      const { gamesMap } = GLOBAL_DATA;
+      const gamesSpecialsMap = {};
+      specials.forEach(special => {
+        const { eventId } = special;
+        if (!gamesSpecialsMap[eventId]) {
+          gamesSpecialsMap[eventId] = {};
+        }
+        if (special.name == 'Winning Margin') {
+          gamesSpecialsMap[eventId].winningMargin = special;
+        }
+        else if (special.name == 'Exact Total Goals') {
+          gamesSpecialsMap[eventId].exactTotalGoals = special;
+        }
+      });
+
+      Object.keys(gamesSpecialsMap).forEach(eventId => {
+        if (!gamesMap[eventId]) {
+          return;
+        }
+
+        if (!gamesMap[eventId].specials) {
+          gamesMap[eventId].specials = {};
+        }
+
+        const localSpecials = gamesMap[eventId].specials;
+        const remoteSpecials = gamesSpecialsMap[eventId];
+
+        mergeSpecials(localSpecials, remoteSpecials, 'winningMargin');
+        mergeSpecials(localSpecials, remoteSpecials, 'exactTotalGoals');
+
+      });
+    }
+    return Promise.resolve('SpecialFixtures');
+  });
+}
+
+/**
+ * 获取特殊赛赔率数据
+ * @returns
+ */
+const getSpecialsOdds = async () => {
+  if (!GLOBAL_DATA.filteredLeagues.length) {
+    resetVersionsCount();
+    return Promise.reject(new Error('no filtered leagues', { cause: 400 }));
+  }
+  const leagueIds = GLOBAL_DATA.filteredLeagues.join(',');
+  const since = GLOBAL_DATA.specialsOddsVersion;
+  // if (GLOBAL_DATA.specialsOddsCount >= 33) {
+  //   since = 0;
+  //   GLOBAL_DATA.specialsOddsCount = 9;
+  // }
+  if (since == 0) {
+    Logs.outDev('full update specials odds');
+  }
+  return pinnacleGet('/v2/odds/special', { sportId: 29, oddsFormat: 'Decimal', leagueIds, since })
+  .then(data => {
+    const { leagues, last } = data;
+    if (!last) {
+      return [];
+    }
+    GLOBAL_DATA.specialsOddsVersion = last;
+    return leagues?.flatMap(league => league.specials);
+  });
+}
+
+/**
+ * 更新特殊赛赔率数据
+ * @returns
+ */
+const updateSpecialsOdds = async () => {
+  return getSpecialsOdds()
+  .then(specials => {
+    if (specials?.length) {
+      const { gamesMap } = GLOBAL_DATA;
+      const contestants = Object.values(gamesMap)
+      .filter(game => game.specials)
+      .map(game => {
+        const { specials } = game;
+        const contestants = Object.values(specials).map(special => {
+          const { contestants } = special;
+          return contestants.map(contestant => [contestant.id, contestant]);
+        });
+        return contestants;
+      }).flat(2);
+      const contestantsMap = new Map(contestants);
+      const lines = specials.flatMap(special => special.contestantLines);
+      lines.forEach(line => {
+        const { id, handicap, lineId, max, price } = line;
+        const contestant = contestantsMap.get(id);
+        if (!contestant) {
+          return;
+        }
+        contestant.handicap = handicap;
+        contestant.lineId = lineId;
+        contestant.max = max;
+        contestant.price = price;
+      });
+    }
+    return Promise.resolve('SpecialsOdds');
+  });
+}
+
+/**
+ * 获取滚球数据
+ * @returns
+ */
+const getInRunning = async () => {
+  return pinnacleGet('/v2/inrunning')
+  .then(data => {
+    const sportId = 29;
+    const leagues = data.sports?.find(sport => sport.id == sportId)?.leagues ?? [];
+    return leagues.filter(league => {
+      if (!GLOBAL_DATA.filteredLeagues.length) {
+        return true;
+      }
+      const { id } = league;
+      const filteredLeaguesSet = new Set(GLOBAL_DATA.filteredLeagues);
+      return filteredLeaguesSet.has(id);
+    }).flatMap(league => league.events);
+  });
+}
+
+/**
+ * 更新滚球数据
+ * @returns
+ */
+const updateInRunning = async () => {
+  return getInRunning()
+  .then(games => {
+    if (!games.length) {
+      return Promise.resolve('InRunning');
+    }
+    const { gamesMap } = GLOBAL_DATA;
+    games.forEach(game => {
+      const { id, state, elapsed } = game;
+      const localGame = gamesMap[id];
+      if (localGame) {
+        Object.assign(localGame, { state, elapsed });
+      }
+    });
+    return Promise.resolve('InRunning');
+  });
+}
+
+/**
+ * 获取比赛盘口赔率数据
+ * @returns {Object}
+ */
+const getGamesEvents = () => {
+  const {
+    filteredGames, gamesMap,
+    straightFixturesVersion: sfv,
+    specialFixturesVersion: pfv,
+    straightOddsVersion: sov,
+    specialsOddsVersion: pov
+  } = GLOBAL_DATA;
+  const filteredGamesSet = new Set(filteredGames);
+  const nowTime = Date.now();
+  const gamesData = {};
+  Object.values(gamesMap).forEach(game => {
+    const { id, liveStatus, parentId, resultingUnit, timestamp } = game;
+
+    if (resultingUnit !== 'Regular') {
+      return false;  // 非常规赛事不处理
+    }
+
+    const gmtMinus4Date = getDateInTimezone(-4);
+    const todayEndTime = new Date(`${gmtMinus4Date} 23:59:59 GMT-4`).getTime();
+    const tomorrowEndTime = todayEndTime + 24 * 60 * 60 * 1000;
+    if (liveStatus == 1 && timestamp < nowTime) {
+      game.marketType = 2;  // 滚球赛事
+    }
+    else if (liveStatus != 1 && timestamp > nowTime && timestamp <= todayEndTime) {
+      game.marketType = 1;  // 今日赛事
+    }
+    else if (liveStatus != 1 && timestamp > todayEndTime && timestamp <= tomorrowEndTime) {
+      game.marketType = 0;  // 明日早盘赛事
+    }
+    else {
+      game.marketType = -1;  // 非近期赛事
+    }
+
+    if (game.marketType < 0) {
+      return false;  // 非近期赛事不处理
+    }
+
+    let actived = false;
+    if (liveStatus != 1 && (filteredGamesSet.has(id) || !filteredGames.length)) {
+      actived = true;  // 在赛前列表中
+      game.id = id;
+      game.originId = 0;
+    }
+    else if (liveStatus == 1 && (filteredGamesSet.has(parentId) || !filteredGames.length)) {
+      actived = true;  // 在滚球列表中
+      game.id = parentId;
+      game.originId = id;
+    }
+
+    if (actived) {
+      const { marketType, ...rest } = parseGame(game);
+      if (!gamesData[marketType]) {
+        gamesData[marketType] = [];
+      }
+      gamesData[marketType].push(rest);
+    }
+  });
+  const games = Object.values(gamesData).flat();
+  const timestamp = Math.max(sfv, pfv, sov, pov);
+  const data = { games, timestamp };
+  return data;
+}
+
+/**
+ * 更新赔率数据
+ */
+const updateOdds = async () => {
+  const { games, timestamp } = getGamesEvents();
+  return platformPost('/api/platforms/update_odds', { platform: 'pinnacle', games, timestamp });
+}
+
+
+const pinnacleDataLoop = () => {
+  updateStraightFixtures()
+  .then(() => {
+    return Promise.all([
+      updateStraightOdds(),
+      updateSpecialFixtures(),
+      updateInRunning(),
+    ]);
+  })
+  .then(() => {
+    return updateSpecialsOdds();
+  })
+  .then(() => {
+    if (!GLOBAL_DATA.loopActive) {
+      GLOBAL_DATA.loopActive = true;
+      Logs.out('loop active');
+      notifyException('Pinnacle API startup.');
+    }
+
+    if (GLOBAL_DATA.requestErrorCount > 0) {
+      GLOBAL_DATA.requestErrorCount = 0;
+      Logs.out('request error count reset');
+    }
+
+    const nowTime = Date.now();
+    const loopDuration = nowTime - GLOBAL_DATA.loopLastResultTime;
+    GLOBAL_DATA.loopLastResultTime = nowTime;
+    if (loopDuration > 15000) {
+      Logs.out('loop duration is too long', loopDuration);
+    }
+    else {
+      Logs.outDev('loop duration', loopDuration);
+    }
+    setData(gamesMapCacheFile, GLOBAL_DATA.gamesMap);
+
+    return updateOdds();
+  })
+  .catch(err => {
+    Logs.err(err.message, err.source);
+    GLOBAL_DATA.requestErrorCount++;
+    if (GLOBAL_DATA.loopActive && GLOBAL_DATA.requestErrorCount > 5) {
+      const exceptionMessage = 'request errors have reached the limit';
+      Logs.out(exceptionMessage);
+      GLOBAL_DATA.loopActive = false;
+
+      Logs.out('loop inactive');
+      notifyException(`Pinnacle API paused. ${exceptionMessage}. ${err.message}`);
+    }
+  })
+  .finally(() => {
+    const { loopActive } = GLOBAL_DATA;
+    let loopDelay = 1000 * 5;
+    if (!loopActive) {
+      loopDelay = 1000 * 60;
+      resetVersionsCount();
+    }
+    else {
+      incrementVersionsCount();
+    }
+    setTimeout(pinnacleDataLoop, loopDelay);
+  });
+}
+
+
+/**
+ * 缓存GLOBAL_DATA数据到文件
+ */
+const saveGlobalDataToCache = async () => {
+  return setData(globalDataCacheFile, GLOBAL_DATA);
+}
+
+const loadGlobalDataFromCache = async () => {
+  return getData(globalDataCacheFile)
+  .then(data => {
+    if (!data) {
+      return;
+    }
+    Object.keys(GLOBAL_DATA).forEach(key => {
+      if (key in data) {
+        GLOBAL_DATA[key] = data[key];
+      }
+    });
+  });
+}
+
+const main = () => {
+  if (!process.env.PINNACLE_USERNAME || !process.env.PINNACLE_PASSWORD) {
+    Logs.err('USERNAME or PASSWORD is not set');
+    return;
+  }
+  updateFilteredLeagues();
+  updateFilteredGames();
+  loadGlobalDataFromCache()
+  .then(() => {
+    Logs.out('global data loaded');
+  })
+  .then(() => {
+    updateWebLeaguesLoop();
+    updateWebGamesLoop();
+    pinnacleDataLoop();
+  })
+  .catch(err => {
+    Logs.err('failed to load global data', err.message);
+  })
+  .finally(() => {
+    GLOBAL_DATA.loopLastResultTime = Date.now();
+    GLOBAL_DATA.loopActive = true;
+  });
+}
+
+// 监听进程退出事件,保存GLOBAL_DATA数据
+const saveExit = (code) => {
+  saveGlobalDataToCache()
+  .then(() => {
+    Logs.out('global data saved');
+  })
+  .catch(err => {
+    Logs.err('failed to save global data', err.message);
+  })
+  .finally(() => {
+    process.exit(code);
+  });
+}
+process.on('SIGINT', () => {
+  saveExit(0);
+});
+process.on('SIGTERM', () => {
+  saveExit(0);
+});
+process.on('SIGUSR2', () => {
+  saveExit(0);
+});
+
+main();

+ 383 - 0
pinnacle/package-lock.json

@@ -0,0 +1,383 @@
+{
+  "name": "pinnacle",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "pinnacle",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "axios": "^1.13.3",
+        "dayjs": "^1.11.19",
+        "dotenv": "^17.2.3",
+        "https-proxy-agent": "^7.0.6",
+        "ws": "^8.19.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.13.3",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.3.tgz",
+      "integrity": "sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.4",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.19",
+      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz",
+      "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "17.2.3",
+      "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.2.3.tgz",
+      "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
+    "node_modules/ws": {
+      "version": "8.19.0",
+      "resolved": "https://registry.npmmirror.com/ws/-/ws-8.19.0.tgz",
+      "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    }
+  }
+}

+ 19 - 0
pinnacle/package.json

@@ -0,0 +1,19 @@
+{
+  "name": "pinnacle",
+  "version": "1.0.0",
+  "description": "",
+  "main": "main.js",
+  "type": "module",
+  "scripts": {
+    "dev": "nodemon --ignore data/ --ignore cache/ --ignore node_modules/ --inspect=9228 main.js"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "axios": "^1.13.3",
+    "dayjs": "^1.11.19",
+    "dotenv": "^17.2.3",
+    "https-proxy-agent": "^7.0.6",
+    "ws": "^8.19.0"
+  }
+}

+ 46 - 0
polymarket/libs/cache.js

@@ -0,0 +1,46 @@
+import fs from 'fs';
+import path from 'path';
+
+export const getData = (file) => {
+  let data = null;
+
+  if (fs.existsSync(file)) {
+    const arrayBuffer = fs.readFileSync(file);
+
+    try {
+      data = JSON.parse(arrayBuffer.toString());
+    }
+    catch (e) {}
+  }
+
+  return Promise.resolve(data);
+}
+
+export const setData = (file, data, indent = 2) => {
+  return new Promise((resolve, reject) => {
+
+    if (typeof (data) != 'string') {
+      try {
+        data = JSON.stringify(data, null, indent);
+      }
+      catch (error) {
+        reject(error);
+      }
+    }
+
+    const directoryPath = path.dirname(file);
+    if(!fs.existsSync(directoryPath)) {
+      fs.mkdirSync(directoryPath, { recursive: true });
+    }
+
+    try {
+      fs.writeFileSync(file, data);
+      resolve();
+    }
+    catch (error) {
+      reject(error);
+    }
+  });
+}
+
+export default { getData, setData };

+ 79 - 0
polymarket/libs/getDateInTimezone.js

@@ -0,0 +1,79 @@
+/**
+ * 将时区偏移小时转换为 ±HHMM 格式
+ * @param {number|string} offset 如 8, -4, "+8", "-04"
+ * @returns {string} 如 "+0800", "-0400"
+ */
+const formatTimezoneOffset = (offset) => {
+  const hours = Number(offset);
+  if (Number.isNaN(hours)) {
+    throw new Error('Invalid timezone offset');
+  }
+
+  const sign = hours >= 0 ? '+' : '-';
+  const hh = String(Math.abs(hours)).padStart(2, '0');
+
+  return `${sign}${hh}00`;
+}
+
+
+/**
+ * 判断日期是否无效
+ * @param {Date} date
+ * @returns {boolean}
+ */
+const isInvalidDate = (date) => {
+  return date instanceof Date && Number.isNaN(date.getTime());
+}
+
+
+/**
+ * 获取指定时区当前日期或时间
+ * @param {number} offset - 时区相对 UTC 的偏移(例如:-4 表示 GMT-4)
+ * @param {number} timestamp - 时间戳(可选)
+ * @param {boolean} [withTime=false] - 是否返回完整时间(默认只返回日期)
+ * @returns {string} 格式化的日期或时间字符串
+ */
+const getDateInTimezone = (offset, timestamp, withTime=false) => {
+
+  offset = Number(offset);
+  if (Number.isNaN(offset)) {
+    throw new Error('Invalid timezone offset');
+  }
+
+  if (typeof(timestamp) === 'undefined') {
+    timestamp = Date.now();
+  }
+  else if (typeof(timestamp) === 'boolean') {
+    withTime = timestamp;
+    timestamp = Date.now();
+  }
+  else if (typeof(timestamp) === 'string') {
+    const date = new Date(timestamp);
+    if (isInvalidDate(date)) {
+      throw new Error('Invalid timestamp');
+    }
+    timestamp = date.getTime();
+  }
+  else if (typeof(timestamp) !== 'number') {
+    throw new Error('Invalid timestamp');
+  }
+
+  const nowUTC = new Date(timestamp);
+  const targetTime = new Date(nowUTC.getTime() + offset * 60 * 60 * 1000);
+
+  const year = targetTime.getUTCFullYear();
+  const month = String(targetTime.getUTCMonth() + 1).padStart(2, '0');
+  const day = String(targetTime.getUTCDate()).padStart(2, '0');
+
+  if (!withTime) {
+    return `${year}-${month}-${day}`;
+  }
+
+  const hours = String(targetTime.getUTCHours()).padStart(2, '0');
+  const minutes = String(targetTime.getUTCMinutes()).padStart(2, '0');
+  const seconds = String(targetTime.getUTCSeconds()).padStart(2, '0');
+
+  return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${formatTimezoneOffset(offset)}`;
+}
+
+export default getDateInTimezone;

+ 45 - 0
polymarket/libs/logs.js

@@ -0,0 +1,45 @@
+import dayjs from 'dayjs';
+
+export default class Logs {
+
+  static out(...args) {
+    const timeString = dayjs().format('YYYY-MM-DD HH:mm:ss.SSS');
+    if (typeof args[0] === 'string' && args[0].includes('%')) {
+      args[0] = `[${timeString}] ` + args[0];
+    }
+    else {
+      args.unshift(`[${timeString}]`);
+    }
+    console.log(...args);
+  }
+
+  static err(...args) {
+    const timeString = dayjs().format('YYYY-MM-DD HH:mm:ss.SSS');
+    if (typeof args[0] === 'string' && args[0].includes('%')) {
+      args[0] = `[${timeString}] ` + args[0];
+    }
+    else {
+      args.unshift(`[${timeString}]`);
+    }
+    console.error(...args);
+  }
+
+  static outDev(...args) {
+    if (process.env.NODE_ENV == 'development') {
+      this.out(...args);
+    }
+  }
+
+  static errDev(...args) {
+    if (process.env.NODE_ENV == 'development') {
+      this.err(...args);
+    }
+  }
+
+  static outLine(string) {
+    process.stdout.write("\u001b[1A");
+    process.stdout.write("\u001b[2K");
+    this.out(string);
+  }
+
+}

+ 310 - 0
polymarket/libs/parseMarkets.js

@@ -0,0 +1,310 @@
+import getDateInTimezone from "./getDateInTimezone.js";
+
+/**
+ * 精确浮点数字
+ * @param {number} number
+ * @param {number} x
+ * @returns {number}
+ */
+const fixFloat = (number, x=3) => {
+  return parseFloat(number.toFixed(x));
+}
+
+/**
+ * 清理对象中的undefined值
+ * @param {*} obj
+ * @returns {Object}
+ */
+const cleanUndefined = (obj) => {
+  return Object.fromEntries(
+    Object.entries(obj).filter(([, v]) => v !== undefined)
+  );
+}
+
+/**
+ * 解析赛事标题
+ * @param {*} eventTitle
+ * @returns {Object}
+ */
+const parseTeamData = (eventTitle) => {
+  let titleSpliter;
+  if (eventTitle.includes(' vs. ')) {
+    titleSpliter = ' vs. ';
+  }
+  else if (eventTitle.includes(' vs ')) {
+    titleSpliter = ' vs ';
+  }
+  if (!titleSpliter) {
+    return {
+      teamHomeName: '',
+      teamAwayName: '',
+    };
+  }
+  const teamData = eventTitle.replace(' - More Markets', '').split(titleSpliter);
+  return {
+    teamHomeName: teamData[0],
+    teamAwayName: teamData[1],
+  };
+}
+
+/**
+ * 解析盘口键值对
+ * 示例:
+ * {
+ *   "Yes": 0.525,
+ *   "No": 0.475
+ * }
+ * @param {*} outcomes
+ * @param {*} outcomePrices
+ * @returns {Object}
+ */
+const parseOutcomes = (outcomes, clobTokenIds, bestBid=0, bestAsk=0) => {
+  const keys = JSON.parse(outcomes);
+  const ids = JSON.parse(clobTokenIds);
+  return keys.reduce((obj, key, index) => {
+    let best_ask;
+    let best_bid;
+    if (index === 0) {
+      best_ask = (bestAsk).toFixed(3);
+      best_bid = (bestBid).toFixed(3);
+    }
+    else if (index === 1) {
+      best_ask = (1 - bestBid).toFixed(3);
+      best_bid = (1 - bestAsk).toFixed(3);
+    }
+    obj[key] = { id: ids[index], best_ask, best_bid };
+    return obj;
+  }, {});
+}
+
+/**
+ * 解析盘口数据
+ * @param {*} markets
+ * @returns {Object}
+ */
+const ratioAccept = (ratio) => {
+  if (ratio > 0) {
+    return 'a';
+  }
+  return '';
+}
+const ratioString = (ratio) => {
+  ratio = Math.abs(ratio);
+  ratio = ratio.toString();
+  ratio = ratio.replace(/\./, '');
+  return ratio;
+}
+export const parseOdds = (markets) => {
+  const odds = {};
+  Object.keys(markets).forEach(key => {
+    const marketData = markets[key];
+    if (key === 'moneyline') {
+      Object.keys(marketData).forEach(side => {
+        const askYes = marketData[side].outcomes['Yes']['best_ask'];
+        const askNo = marketData[side].outcomes['No']['best_ask'];
+        if (askYes <= 0.1 || askNo <= 0.1) {
+          return;
+        }
+        const iorYes = fixFloat(1 / askYes);
+        const iorNo = fixFloat(1 / askNo);
+        let iorKeyYes = '';
+        let iorKeyNo = '';
+        switch (side) {
+          case 'Home':
+            iorKeyYes = 'ior_mh';
+            iorKeyNo = 'ior_moh';
+            break;
+          case 'Draw':
+            iorKeyYes = 'ior_mn';
+            iorKeyNo = 'ior_mon';
+            break;
+          case 'Away':
+            iorKeyYes = 'ior_mc';
+            iorKeyNo = 'ior_moc';
+            break;
+        }
+        odds[iorKeyYes] = { v: iorYes, ask: askYes };
+        odds[iorKeyNo] = { v: iorNo, ask: askNo };
+      });
+    }
+    else if (key === 'spreads') {
+      Object.keys(marketData).forEach(handicap => {
+        const ratio = +handicap;
+        const askHome = marketData[handicap].outcomes['Home']['best_ask'];
+        const askAway = marketData[handicap].outcomes['Away']['best_ask'];
+        if (askHome <= 0.1 || askAway <= 0.1) {
+          return;
+        }
+        const iorHome = fixFloat(1 / askHome);
+        const iorAway = fixFloat(1 / askAway);
+        odds[`ior_r${ratioAccept(ratio)}h_${ratioString(ratio)}`] = { v: iorHome, ask: askHome };
+        odds[`ior_r${ratioAccept(-ratio)}c_${ratioString(ratio)}`] = { v: iorAway, ask: askAway };
+      });
+    }
+    else if (key === 'totals') {
+      Object.keys(marketData).forEach(handicap => {
+        const ratio = +handicap;
+        const askOver = marketData[handicap].outcomes['Over']['best_ask'];
+        const askUnder = marketData[handicap].outcomes['Under']['best_ask'];
+        if (askOver <= 0.1 || askUnder <= 0.1) {
+          return;
+        }
+        const iorOver = fixFloat(1 / askOver);
+        const iorUnder = fixFloat(1 / askUnder);
+        odds[`ior_ouc_${ratioString(ratio)}`] = { v: iorOver, ask: askOver };
+        odds[`ior_ouh_${ratioString(ratio)}`] = { v: iorUnder, ask: askUnder };
+      });
+    }
+  });
+  return odds;
+}
+
+/**
+ * 解析胜平负盘口
+ * @param {*} groupItemThreshold
+ * @param {*} outcomesMap
+ * @param {*} market
+ * @returns {Object}
+ */
+const parseMoneyline = (groupItemTitle, outcomesMap, teams, market) => {
+  const { teamHomeName, teamAwayName } = teams;
+  let key = '';
+  if (groupItemTitle == teamHomeName) {
+    key = 'Home';
+  }
+  else if (groupItemTitle == teamAwayName) {
+    key = 'Away';
+  }
+  else if (groupItemTitle.startsWith('Draw')) {
+    key = 'Draw';
+  }
+  if (!key) {
+    return {};
+  }
+  return { [key]: { outcomes: outcomesMap, market } };
+};
+
+/**
+ * 解析让球盘口
+ * @param {*} groupItemTitle
+ * @param {*} spreadsLine
+ * @param {*} outcomesMap
+ * @param {*} teams
+ * @param {*} market
+ * @returns {Object}
+ */
+const parseSpreads = (groupItemTitle, spreadsLine, outcomesMap, teams, market) => {
+  const { teamHomeName, teamAwayName } = teams;
+  let spreadDirection = 0;
+  if (groupItemTitle.includes(teamHomeName)) {
+    spreadDirection = 1;
+  }
+  else if (groupItemTitle.includes(teamAwayName)) {
+    spreadDirection = -1;
+  }
+  const spreads = spreadsLine * spreadDirection;
+  const key = spreads > 0 ? `+${spreads}` : `${spreads}`;
+  const spreadsMap = {};
+  Object.keys(outcomesMap).forEach(key => {
+    if (key == teamHomeName) {
+      spreadsMap['Home'] = outcomesMap[key];
+    }
+    else if (key == teamAwayName) {
+      spreadsMap['Away'] = outcomesMap[key];
+    }
+  });
+  return { [key]: { outcomes: spreadsMap, market } };
+};
+
+/**
+ * 解析大小盘口
+ * @param {*} line
+ * @param {*} outcomesMap
+ * @param {*} market
+ * @returns {Object}
+ */
+const parseTotals = (line, outcomesMap, market) => {
+  return { [line]: { outcomes: outcomesMap, market } };
+};
+
+/**
+ * 解析市场数据
+ * @param {*} markets
+ * @param {*} teams
+ * @returns {Object}
+ */
+const parseMarketsData = (markets, teams) => {
+  const marketsData = {};
+  markets.forEach(market => {
+    const { sportsMarketType, groupItemTitle, line, outcomes, clobTokenIds, bestBid=0, bestAsk=0 } = market;
+    const outcomesMap = parseOutcomes(outcomes, clobTokenIds, bestBid, bestAsk);
+    if (sportsMarketType === "moneyline") {
+      if (!marketsData.moneyline) {
+        marketsData.moneyline = {};
+      }
+      Object.assign(marketsData.moneyline, parseMoneyline(groupItemTitle, outcomesMap, teams, market))
+    }
+    else if (sportsMarketType === "spreads") {
+      if (!marketsData.spreads) {
+        marketsData.spreads = {};
+      }
+      Object.assign(marketsData.spreads, parseSpreads(groupItemTitle, line, outcomesMap, teams, market))
+    }
+    else if (sportsMarketType === "totals") {
+      if (!marketsData.totals) {
+        marketsData.totals = {};
+      }
+      Object.assign(marketsData.totals, parseTotals(line, outcomesMap, market));
+    }
+  });
+  return marketsData;
+}
+
+/**
+ * 解析赛事数据
+ * @param {*} event
+ * @returns {Object}
+ */
+const parseEvent = (event) => {
+  const { id, title, series, gameId, parentEventId, startTime } = event;
+  const { teamHomeName, teamAwayName } = parseTeamData(title);
+  const leagueId = series[0].id;
+  const leagueName = series[0].title;
+  const timestamp = new Date(startTime).getTime();
+  return {
+    id: +id,
+    gameId: gameId ? +gameId : undefined,
+    parentEventId: parentEventId ? +parentEventId : undefined,
+    leagueId: +leagueId,
+    teamHomeName, teamAwayName, leagueName,
+    timestamp,
+    startTime: getDateInTimezone('+8', timestamp, true),
+  };
+}
+
+/**
+ * 解析市场列表数据
+ * @param {*} events
+ * @returns {Object}
+ */
+export const parseMarkets = (eventsData) => {
+  const mergedMarketsData = {};
+  Object.values(eventsData).map(event => {
+    const item = parseEvent(event);
+    const { markets } = event;
+    const { teamHomeName, teamAwayName } = item;
+    item.marketsData = parseMarketsData(markets, { teamHomeName, teamAwayName });
+    return item;
+  }).sort((a, b) => a.id - b.id).forEach(item => {
+    if (item.id && !item.parentEventId) {
+      mergedMarketsData[item.id] = cleanUndefined(item);
+    }
+    else if (item.parentEventId && mergedMarketsData[item.parentEventId] && item.marketsData) {
+      const { marketsData: parentEvents } = mergedMarketsData[item.parentEventId];
+      Object.assign(parentEvents, item.marketsData);
+    }
+  });
+  return mergedMarketsData;
+}
+
+// export default parseMarkets;

+ 272 - 0
polymarket/libs/polymarketClient.js

@@ -0,0 +1,272 @@
+import axios from "axios";
+import qs from "qs";
+import { HttpsProxyAgent } from "https-proxy-agent";
+import { ClobClient, Side, OrderType } from "@polymarket/clob-client";
+import { Wallet } from "ethers";
+
+import WebSocketClient from "./WebSocketClient.js";
+
+import Logs from "./logs.js";
+
+/**
+ * axios 默认配置
+ */
+const axiosDefaultOptions = {
+  baseURL: "",
+  url: "",
+  method: "GET",
+  headers: {},
+  params: {},
+  data: {},
+  timeout: 10000,
+};
+
+/**
+ * 通用请求
+ * @param {*} options
+ * @param {*} baseURL
+ * @returns
+ */
+const clientRequest = async (options, baseURL) => {
+  const { url } = options;
+  if (!url || !baseURL) {
+    throw new Error("url and baseURL are required");
+  }
+  const mergedOptions = {
+    ...axiosDefaultOptions,
+    ...options,
+    baseURL,
+    paramsSerializer: params => qs.stringify(params, { arrayFormat: 'repeat' })
+  };
+  const proxy = process.env.NODE_HTTP_PROXY;
+  if (proxy) {
+    mergedOptions.proxy = false;
+    mergedOptions.httpsAgent = new HttpsProxyAgent(proxy);
+  }
+  return axios(mergedOptions).then(res => res.data);
+}
+
+/**
+ * 请求市场数据
+ * @param {*} options
+ * @returns
+ */
+const requestMarketData = async (options) => {
+  return clientRequest(options, "https://gamma-api.polymarket.com");
+}
+
+/**
+ * 请求订单簿数据
+ */
+const requestClobData = async (options) => {
+  return clientRequest(options, "https://clob.polymarket.com");
+}
+
+/**
+ * 获取足球联赛
+ * @returns {Promise}
+ */
+export const getSoccerSports = async () => {
+  return requestMarketData({ url: "/sports" })
+  .then(sportsData => {
+    return sportsData.filter(item => {
+      const { tags } = item;
+      const tagIds = tags.split(",").map(item => +item);
+      return tagIds.includes(100350);
+    });
+  });
+}
+
+/**
+ * 获取赛事数据
+ * @param {Object} options
+ * @param {number} options.limit
+ * @param {number} options.tag_id
+ * @param {boolean} options.active
+ * @param {boolean} options.closed
+ * @param {string} options.endDateMin
+ * @param {string} options.endDateMax
+ * @returns {Promise}
+ */
+export const getEvents = async ({
+  limit = 500, tag_id = 100350, active = true,
+  closed = false, endDateMin = "", endDateMax = "",
+} = {}) => {
+  return requestMarketData({
+    url: "/events",
+    params: {
+      limit, tag_id,  active, closed,
+      end_date_min: endDateMin,
+      end_date_max: endDateMax,
+    }
+  }).then(events => events.filter(item => !!item.series));
+}
+
+/**
+ * 获取订单簿数据
+ */
+export const getOrderBook = async (tokenId) => {
+  return requestClobData({
+    url: "/book",
+    params: {
+      token_id: tokenId,
+    }
+  });
+}
+
+/**
+ * 批量获取订单簿数据
+ * @param {Array} tokenIds
+ * @returns {Promise}
+ */
+export const getMultipleOrderBooks = async (tokenIds) => {
+  return requestClobData({
+    url: "/books",
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: tokenIds.map(tokenId => ({ token_id: tokenId })),
+  });
+}
+
+/**
+ * 下注
+ */
+export const placeOrder = async (info) => {
+  // return Promise.resolve(info);
+  const { asset_id, bestPrice, stakeSize, tick_size: tickSize, neg_risk: negRisk } = info;
+
+  const HOST = "https://clob.polymarket.com";
+  const CHAIN_ID = 137; // Polygon mainnet
+  const signer = new Wallet(process.env.POLYMARKET_PRIVATE_KEY);
+  const userApiCreds = {
+    key: process.env.POLYMARKET_API_KEY,
+    secret: process.env.POLYMARKET_API_SECRET,
+    passphrase: process.env.POLYMARKET_API_PASSPHRASE,
+  };
+
+  const SIGNATURE_TYPE = 0;
+  const FUNDER_ADDRESS = signer.address;
+
+  // Logs.outDev('clob client init', HOST, CHAIN_ID, signer.address, userApiCreds, SIGNATURE_TYPE, FUNDER_ADDRESS);
+
+  const client = new ClobClient(
+    HOST,
+    CHAIN_ID,
+    signer,
+    userApiCreds,
+    SIGNATURE_TYPE,
+    FUNDER_ADDRESS
+  );
+
+  const orderData = {
+    tokenID: asset_id,
+    price: bestPrice,
+    size: stakeSize,
+    side: Side.BUY,
+  }
+
+  const orderOptions = { tickSize, negRisk }
+
+  Logs.outDev('polymarket place order data', orderData, orderOptions, OrderType.FOK);
+
+  // return client.getBalanceAllowance({ asset_type: 'COLLATERAL' })
+
+  return client.createAndPostOrder(orderData, orderOptions, OrderType.FOK)
+  .then(res => {
+    // if (res.error) {
+    //   return Promise.reject(new Error(res.error));
+    // }
+    return res;
+  });
+  // return Promise.resolve(info);
+  // return Promise.reject(new Error('polymarket place order not implemented', { cause: 400 }));
+}
+
+
+/**
+ * 请求平台数据
+ * @param {*} options
+ * @returns {Promise}
+ */
+export const platformRequest = async (options) => {
+  const { url } = options;
+  if (!url) {
+    throw new Error("url is required");
+  }
+  const mergedOptions = {
+    ...axiosDefaultOptions,
+    ...options,
+    baseURL: "http://127.0.0.1:9020",
+  };
+  return axios(mergedOptions).then(res => res.data);
+}
+
+/**
+ * 请求平台 POST 数据
+ * @param {string} url
+ * @param {Object} data
+ * @returns {Promise}
+ */
+export const platformPost = async (url, data) => {
+  return platformRequest({
+    url,
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data,
+  });
+}
+
+/**
+ * 请求平台 GET 数据
+ * @param {string} url
+ * @param {Object} params
+ * @returns {Promise}
+ */
+export const platformGet = async (url, params) => {
+  return platformRequest({ url, method: 'GET', params });
+}
+
+/**
+ * 市场 WebSocket 客户端
+ */
+export class MarketWsClient extends WebSocketClient {
+  #assetIds = [];
+  constructor() {
+    let agent;
+    const proxy = process.env.NODE_HTTP_PROXY;
+    if (proxy) {
+      agent = new HttpsProxyAgent(proxy);
+    }
+    super("wss://ws-subscriptions-clob.polymarket.com/ws/market", { agent });
+  }
+
+  connect() {
+    super.connect();
+    this.on('open', () => {
+      if (this.#assetIds.length > 0) {
+        this.subscribeToTokensIds(this.#assetIds);
+      }
+    });
+  }
+
+  subscribeToTokensIds(assetIds) {
+    this.#assetIds = [...new Set([...this.#assetIds, ...assetIds])];
+    this.send({
+      operation: "subscribe",
+      assets_ids: assetIds,
+    });
+  }
+
+  unsubscribeToTokensIds(assetIds) {
+    const assetIdsSet = new Set(assetIds);
+    this.#assetIds = this.#assetIds.filter(id => !assetIdsSet.has(id));
+    this.send({
+      operation: "unsubscribe",
+      assets_ids: assetIds,
+    });
+  }
+}

+ 229 - 0
polymarket/libs/webSocketClient.js

@@ -0,0 +1,229 @@
+import WebSocket from "ws";
+import Logs from "./logs.js";
+
+/**
+ * 解析推送过来的消息
+ * @param {*} data
+ * @returns {object}
+ */
+const parseMessage = (data) => {
+  let message;
+  try {
+    message = JSON.parse(data);
+  }
+  catch(e) {
+    message = { text: data.toString() };
+  }
+  return message;
+}
+
+/**
+ * 自定义Websocket类
+ * 添加自动发送ping指令
+ */
+class WsClient {
+  #websocket;
+  #pingTimer;
+  constructor(wsserver, options) {
+    this.#websocket = new WebSocket(wsserver, options);
+  }
+  get readyState() {
+    return this.#websocket.readyState;
+  }
+  on(type, callback) {
+    switch(type) {
+      case 'open':
+        this.#websocket.on('open', event => {
+          this.#pingTimer = setInterval(() => {
+            if (this.readyState === WebSocket.OPEN) {
+              this.#websocket.send('PING');
+            }
+          }, 10_000);
+          callback(event);
+        });
+        break;
+      case 'message':
+        this.#websocket.on('message', message => {
+          const data = parseMessage(message);
+          callback(data);
+        });
+        break;
+      case 'error':
+        this.#websocket.on('error', error => {
+          clearInterval(this.#pingTimer);
+          callback(error);
+        });
+        break;
+      case 'close':
+        this.#websocket.on('close', code => {
+          clearInterval(this.#pingTimer);
+          callback(code);
+        });
+        break;
+      default:
+        console.log(type, callback);
+    }
+    return this;
+  }
+  send(message) {
+    this.#websocket.send(JSON.stringify(message));
+  }
+  close(code) {
+    this.#websocket.close(code);
+  }
+}
+
+/**
+ * 建立长连接
+ */
+class WebSocketClient {
+  #url;
+  #wsClient = null;
+  #connecting = 0;
+  #retryTimer = null;
+  #retryCount = 0;
+  #options = {};
+  #callback = {};
+
+  constructor(url, options) {
+    this.#url = url;
+    this.#options = options;
+  }
+
+  get wsClient() {
+    return this.#wsClient;
+  }
+
+  get connecting() {
+    return this.#connecting;
+  }
+
+  #setCallback(type, callback) {
+    if (['open', 'message', 'error', 'close'].includes(type)) {
+      this.#callback[type] = callback;
+    }
+    else {
+      throw new Error(`Invalid callback type: ${type}`);
+    }
+  }
+
+  #runCallback(type, event) {
+    if (this.#callback[type]) {
+      this.#callback[type](event);
+    }
+  }
+
+  /**
+   * 建立连接(若已在连接中/已连接,会先关闭旧连接再重建)
+   * @returns {WsClient}
+   */
+  connect() {
+    if (this.#retryTimer) {
+      clearTimeout(this.#retryTimer);
+      this.#retryTimer = null;
+    }
+
+    // const { url, onmessage } = this.#connectInfo;
+    const wsClient = new WsClient(this.#url, this.#options);
+    this.#wsClient = wsClient;
+    this.#connecting = 1;
+
+    wsClient.on('open', event => {
+      this.#retryCount = 0;
+      this.#connecting = 0;
+      if (this.#retryTimer) {
+        clearTimeout(this.#retryTimer);
+        this.#retryTimer = null;
+      }
+      this.#runCallback('open', event);
+      // Logs.outDev('connect success');
+    });
+
+    wsClient.on('message', data => {
+      const message = data;
+      if (message.toUpperCase?.() == 'PONG' || message?.text?.toUpperCase?.() == 'PONG') {
+        return;
+      }
+      this.#runCallback('message', data);
+      // Logs.outDev('receive message', message);
+    });
+
+    wsClient.on('error', event => {
+      Logs.outDev('connect error', event);
+      this.#runCallback('error', event);
+    });
+
+    wsClient.on('close', event => {
+      this.#runCallback('close', event);
+      if (event.code == 1000) { // 手动断开,直接结束
+        this.#wsClient = null;
+        this.#connecting = 0;
+        if (this.#retryTimer) {
+          clearTimeout(this.#retryTimer);
+          this.#retryTimer = null;
+        }
+        Logs.outDev('close connection autonomously');
+        return;
+      }
+
+      // 异常断开,重新尝试连接
+      this.#retryCount += 1;
+      if (this.#retryCount > 180) {
+        this.#wsClient = null;
+        this.#connecting = 0;
+        Logs.outDev('ws connect retry reached maximum limit');
+        return;
+      }
+
+      let timeout = 5;
+      if (this.#retryCount > 24) {
+        timeout = 30;
+      }
+      else if (this.#retryCount > 40) {
+        timeout = 60;
+      }
+
+      this.#connecting = 1;
+      this.#retryTimer = setTimeout(() => {
+        this.connect();
+      }, 1000 * timeout);
+      Logs.outDev('connect closed, try again after %d seconds', timeout);
+    });
+
+    return wsClient;
+  }
+
+  /**
+   * 手动关闭连接(默认 code=1000)
+   */
+  close(code = 1000) {
+    if (this.#retryTimer) {
+      clearTimeout(this.#retryTimer);
+      this.#retryTimer = null;
+    }
+    this.#retryCount = 0;
+    this.#connecting = 0;
+    this.#wsClient?.close(code);
+  }
+
+  /**
+   * 发送消息
+   * @param {*} message
+   */
+  send(message) {
+    this.#wsClient?.send(message);
+  }
+
+  /**
+   * 绑定事件
+   * @param {*} type
+   * @param {*} callback
+   * @returns
+   */
+  on(type, callback) {
+    this.#setCallback(type, callback);
+    return this;
+  }
+}
+
+export default WebSocketClient;

+ 357 - 0
polymarket/main.js

@@ -0,0 +1,357 @@
+import path from "path";
+import { fileURLToPath } from "url";
+
+import Logs from "./libs/logs.js";
+import { setData } from "./libs/cache.js";
+import getDateInTimezone from "./libs/getDateInTimezone.js";
+
+import { getSoccerSports, getEvents, platformPost, platformGet, MarketWsClient } from "./libs/polymarketClient.js";
+import { parseMarkets, parseOdds } from "./libs/parseMarkets.js";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const eventsCacheFile = path.join(__dirname, "./cache/polymarketEventsCache.json");
+const marketsDataFile = path.join(__dirname, "./cache/polymarketMarketsCache.json");
+
+const GLOBAL_DATA = {
+  eventsData: {},
+  marketsData: {},
+  clobTokenMap: {},
+  filteredLeagues: [],
+  filteredGames: [],
+  marketWsClient: null,
+  wsClientData: null,
+  lastChangeTime: 0,
+};
+
+/**
+ * 获取有效联赛
+ * @param {*} marketsList
+ * @param {*} soccerSports
+ * @returns {Array}
+ */
+const getLeagues = (marketsList, soccerSports) => {
+  const soccerSportsMap = new Map(soccerSports.map(item => [+item.series, item]));
+  const leaguesList = marketsList.map(item => {
+    const { leagueId: id, leagueName: name } = item;
+    const sport = soccerSportsMap.get(+id)?.sport;
+    return { id, name, sport };
+  });
+  // 去重并排序
+  const leaguesMap = new Map(leaguesList.map(item => [item.id, item]));
+  return Array.from(leaguesMap.values()).sort((a, b) => a.id - b.id);
+}
+
+// /**
+//  * 获取足球联赛
+//  * @returns {Promise}
+//  */
+// const getSoccerSports = async () => {
+//   return fetchMarketData({ url: "/sports" })
+//   .then(sportsData => {
+//     return sportsData.filter(item => {
+//       const { tags } = item;
+//       const tagIds = tags.split(",").map(item => +item);
+//       return tagIds.includes(100350);
+//     });
+//   });
+// }
+
+/**
+ * 提交联赛和比赛数据
+ * @param {string} platform - 平台名称
+ * @param {string} url - 请求地址
+ * @param {Object} data - 请求数据
+ * @returns {Promise}
+ */
+const submitLeaguesAndGames = async ({ leagues, games }) => {
+  const submitLeagues = platformPost('/api/platforms/update_leagues', { platform: 'polymarket', leagues });
+  const submitGames = platformPost('/api/platforms/update_games', { platform: 'polymarket', games });
+  return Promise.all([submitLeagues, submitGames]);
+}
+
+/**
+ * 更新联赛和比赛数据
+ */
+const updateLeaguesAndGames = (marketsData) => {
+  const marketsList = Object.values(marketsData);
+  getSoccerSports()
+  .then(soccerSports => {
+    const leagues = getLeagues(marketsList, soccerSports);
+    const games = marketsList.map(game => {
+      const { marketsData, ...rest } = game;
+      return rest;
+    }).sort((a, b) => a.timestamp - b.timestamp);
+    return { leagues, games };
+  })
+  .then(data => {
+    Logs.outDev('update leagues and games list', data);
+    return submitLeaguesAndGames(data);
+  })
+  .then(() => {
+    Logs.outDev('leagues and games list updated');
+  })
+  .catch(error => {
+    Logs.out('failed to update leagues and games list', error.message);
+  });
+}
+
+/**
+ * 获取过滤后的比赛数据
+ * @returns
+ */
+const updateFilteredGames = () => {
+  platformGet('/api/platforms/get_filtered_games', { platform: 'polymarket' })
+  .then(res => {
+    const { data: filteredGames } = res;
+    Logs.outDev('filteredGames updated', filteredGames);
+    GLOBAL_DATA.filteredGames = filteredGames.map(item => item.id);
+  })
+  .catch(error => {
+    Logs.out('failed to update filtered games', error.message);
+  })
+  .finally(() => {
+    setTimeout(() => {
+      updateFilteredGames();
+    }, 1000 * 10);
+  });
+}
+
+/**
+ * 获取赛事数据
+ * @returns {Promise}
+ */
+const getMarketsData = async () => {
+  const endDateMinStamps = Date.now() - 1000 * 60 * 60 * 2;
+  const endDateMin = new Date(endDateMinStamps).toISOString();
+  const tomorrowDateMinus4 = getDateInTimezone(-4, Date.now()+24*60*60*1000);
+  const tomorrowGmtMinus4EndTime = new Date(`${tomorrowDateMinus4} 23:59:59 GMT-4`).getTime();
+  const endDateMax = new Date(tomorrowGmtMinus4EndTime).toISOString();
+  return getEvents({ endDateMin, endDateMax })
+  .then(events => {
+    const { eventsData } = GLOBAL_DATA;
+    events.forEach(event => {
+      const { id } = event;
+      if (!eventsData[id]) {
+        eventsData[id] = event;
+      }
+    });
+    const eventsMap = new Map(events.map(event => [event.id, event]));
+    Object.keys(eventsData).forEach(id => {
+      if (!eventsMap.has(id)) {
+        delete eventsData[id];
+      }
+    });
+    setData(eventsCacheFile, eventsData);
+    return parseMarkets(eventsData);
+  })
+  .then(marketsData => {
+
+    updateLeaguesAndGames(marketsData);
+
+    const { marketsData: oldMarketsData } = GLOBAL_DATA;
+    const newMarketsData = marketsData;
+
+    const marketsDataUpdate = {
+      add: [],
+      remove: []
+    }
+
+    Object.keys(oldMarketsData).forEach(id => {
+      if (!newMarketsData[id]) {
+        delete oldMarketsData[id];
+        marketsDataUpdate.remove.push(id);
+      }
+    });
+
+    Object.keys(newMarketsData).forEach(id => {
+      if (!oldMarketsData[id]) {
+        oldMarketsData[id] = newMarketsData[id];
+        marketsDataUpdate.add.push(id);
+      }
+    });
+
+    const clobToken = Object.values(oldMarketsData).map(item => item.marketsData)
+    .flatMap(item => Object.values(item))
+    .flatMap(item => Object.values(item))
+    .flatMap(item => Object.values(item.outcomes));
+
+    const { clobTokenMap: oldClobTokenMap } = GLOBAL_DATA;
+    const newClobTokenMap = new Map(clobToken.map(item => [item.id, item]));
+
+    const clobTokenUpdate = {
+      add: [],
+      remove: []
+    }
+
+    Object.keys(oldClobTokenMap).forEach(id => {
+      if (!newClobTokenMap.has(id)) {
+        delete oldClobTokenMap[id];
+        clobTokenUpdate.remove.push(id);
+      }
+    });
+
+    newClobTokenMap.forEach(item => {
+      const { id } = item;
+      if (!oldClobTokenMap[id]) {
+        oldClobTokenMap[id] = newClobTokenMap.get(id);
+        clobTokenUpdate.add.push(id);
+      }
+    });
+
+    return clobTokenUpdate;
+  });
+}
+
+/**
+ * 循环更新
+ */
+const updateGamesMarkets = () => {
+  Logs.outDev('updateGamesMarkets');
+  getMarketsData()
+  .then(clobTokenUpdate => {
+    const { marketWsClient } = GLOBAL_DATA;
+    const { add, remove } = clobTokenUpdate;
+    if (add.length > 0) {
+      Logs.outDev('subscribeToTokensIds', add);
+      marketWsClient?.subscribeToTokensIds(add);
+    }
+    if (remove.length > 0) {
+      Logs.outDev('unsubscribeToTokensIds', remove);
+      marketWsClient?.unsubscribeToTokensIds(remove);
+    }
+  })
+  .catch(error => {
+    Logs.out('failed to update games markets', error.message);
+  })
+  .finally(() => {
+    setTimeout(() => {
+      updateGamesMarkets();
+    }, 1000 * 60);
+  });
+}
+
+const syncMarketsData = () => {
+  const { marketsData } = GLOBAL_DATA;
+  // Logs.outDev('syncMarketsData', marketsData, clobTokenMap);
+  setData(marketsDataFile, marketsData);
+  setTimeout(() => {
+    syncMarketsData();
+  }, 1000 * 5);
+}
+
+/**
+ * 获取比赛盘口赔率数据
+ * @returns {Object}
+ */
+const getGamesEvents = () => {
+  const { marketsData, lastChangeTime } = GLOBAL_DATA;
+  const games = Object.values(marketsData).map(item => {
+    const { marketsData, ...rest } = item;
+    const odds = parseOdds(marketsData);
+    return { ...rest, odds };
+  }).sort((a, b) => a.timestamp - b.timestamp);
+  return { games, timestamp: lastChangeTime };
+}
+
+/**
+ * 更新赔率数据
+ */
+const updateOdds = async () => {
+  const { games, timestamp } = getGamesEvents();
+  const expireTime = Date.now() - 30_000;
+  if (!games.length || timestamp < expireTime ) {
+    return Promise.resolve();
+  }
+  return platformPost('/api/platforms/update_odds', { platform: 'polymarket', games, timestamp });
+}
+
+/**
+ * 定时更新赔率数据
+ */
+const updateOddsLoop = () => {
+  updateOdds()
+  .catch(error => {
+    Logs.err('failed to update odds', error.message);
+  })
+  .finally(() => {
+    setTimeout(() => {
+      updateOddsLoop();
+    }, 1000 * 2);
+  });
+}
+
+/**
+ * 处理价格变化
+ * @param {*} priceChanges
+ */
+const marketPriceChange = (priceChanges) => {
+  let changed = false;
+  priceChanges.forEach(priceChange => {
+    const { asset_id, best_ask, best_bid } = priceChange;
+    const { clobTokenMap } = GLOBAL_DATA;
+    const clobToken = clobTokenMap[asset_id];
+    if (!clobToken) {
+      return;
+    }
+    Object.assign(clobToken, { best_ask, best_bid });
+    changed = true;
+  });
+  if (changed) {
+    GLOBAL_DATA.lastChangeTime = Date.now();
+  }
+}
+
+/**
+ * 处理市场数据
+ */
+const marketBook = (data) => {
+  const { asks, bids, asset_id } = data;
+  const { clobTokenMap } = GLOBAL_DATA;
+  const clobToken = clobTokenMap[asset_id];
+  if (!clobToken) {
+    return;
+  }
+  const bestAskItem = asks?.reduce((minItem, current) => {
+    return Number(current.price) < Number(minItem.price) ? current : minItem;
+  }, asks?.[0] ?? { price: '1' });
+  const best_ask = bestAskItem?.price ?? '0.001';
+  const bestBidItem = bids?.reduce((maxItem, current) => {
+    return Number(current.price) > Number(maxItem.price) ? current : maxItem;
+  }, bids?.[0] ?? { price: '0' });
+  const best_bid = bestBidItem?.price ?? '0';
+  Object.assign(clobToken, { best_ask, best_bid });
+  GLOBAL_DATA.lastChangeTime = Date.now();
+}
+
+const main = () => {
+  const marketWsClient = new MarketWsClient();
+  GLOBAL_DATA.marketWsClient = marketWsClient;
+  marketWsClient.connect();
+  marketWsClient.on('open', () => {
+    updateFilteredGames();
+    updateGamesMarkets();
+    syncMarketsData();
+    updateOddsLoop();
+  });
+  marketWsClient.on('message', data => {
+    // Logs.outDev('message data:', data);
+    switch(data.event_type) {
+      case 'price_change':
+        marketPriceChange(data.price_changes);
+        break;
+      case 'book':
+        marketBook(data);
+        break
+    }
+  });
+  marketWsClient.on('error', error => {
+    Logs.err('error', error);
+  });
+  marketWsClient.on('close', event => {
+    Logs.outDev('close', event);
+  });
+}
+
+main();

+ 1633 - 0
polymarket/package-lock.json

@@ -0,0 +1,1633 @@
+{
+  "name": "polymarket",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "polymarket",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "@polymarket/clob-client": "^5.2.1",
+        "axios": "^1.13.3",
+        "dayjs": "^1.11.19",
+        "ethers": "^5.8.0",
+        "https-proxy-agent": "^7.0.6",
+        "qs": "^6.14.1",
+        "ws": "^8.19.0"
+      }
+    },
+    "node_modules/@adraffy/ens-normalize": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmmirror.com/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
+      "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
+      "license": "MIT"
+    },
+    "node_modules/@ethersproject/abi": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/abi/-/abi-5.8.0.tgz",
+      "integrity": "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/hash": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/abstract-provider": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz",
+      "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/networks": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "@ethersproject/web": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/abstract-signer": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz",
+      "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-provider": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/address": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/address/-/address-5.8.0.tgz",
+      "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/rlp": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/base64": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/base64/-/base64-5.8.0.tgz",
+      "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/basex": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/basex/-/basex-5.8.0.tgz",
+      "integrity": "sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/bignumber": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/bignumber/-/bignumber-5.8.0.tgz",
+      "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "bn.js": "^5.2.1"
+      }
+    },
+    "node_modules/@ethersproject/bytes": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/bytes/-/bytes-5.8.0.tgz",
+      "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/constants": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/constants/-/constants-5.8.0.tgz",
+      "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/contracts": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/contracts/-/contracts-5.8.0.tgz",
+      "integrity": "sha512-0eFjGz9GtuAi6MZwhb4uvUM216F38xiuR0yYCjKJpNfSEy4HUM8hvqqBj9Jmm0IUz8l0xKEhWwLIhPgxNY0yvQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abi": "^5.8.0",
+        "@ethersproject/abstract-provider": "^5.8.0",
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/hash": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/hash/-/hash-5.8.0.tgz",
+      "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/base64": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/hdnode": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/hdnode/-/hdnode-5.8.0.tgz",
+      "integrity": "sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/basex": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/pbkdf2": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/sha2": "^5.8.0",
+        "@ethersproject/signing-key": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "@ethersproject/wordlists": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/json-wallets": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/json-wallets/-/json-wallets-5.8.0.tgz",
+      "integrity": "sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/hdnode": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/pbkdf2": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/random": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "aes-js": "3.0.0",
+        "scrypt-js": "3.0.1"
+      }
+    },
+    "node_modules/@ethersproject/keccak256": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/keccak256/-/keccak256-5.8.0.tgz",
+      "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "js-sha3": "0.8.0"
+      }
+    },
+    "node_modules/@ethersproject/logger": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/logger/-/logger-5.8.0.tgz",
+      "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/@ethersproject/networks": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/networks/-/networks-5.8.0.tgz",
+      "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/pbkdf2": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz",
+      "integrity": "sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/sha2": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/properties": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/properties/-/properties-5.8.0.tgz",
+      "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/providers": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/providers/-/providers-5.8.0.tgz",
+      "integrity": "sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-provider": "^5.8.0",
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/base64": "^5.8.0",
+        "@ethersproject/basex": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/hash": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/networks": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/random": "^5.8.0",
+        "@ethersproject/rlp": "^5.8.0",
+        "@ethersproject/sha2": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "@ethersproject/web": "^5.8.0",
+        "bech32": "1.1.4",
+        "ws": "8.18.0"
+      }
+    },
+    "node_modules/@ethersproject/providers/node_modules/ws": {
+      "version": "8.18.0",
+      "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.0.tgz",
+      "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@ethersproject/random": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/random/-/random-5.8.0.tgz",
+      "integrity": "sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/rlp": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/rlp/-/rlp-5.8.0.tgz",
+      "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/sha2": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/sha2/-/sha2-5.8.0.tgz",
+      "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "hash.js": "1.1.7"
+      }
+    },
+    "node_modules/@ethersproject/signing-key": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/signing-key/-/signing-key-5.8.0.tgz",
+      "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "bn.js": "^5.2.1",
+        "elliptic": "6.6.1",
+        "hash.js": "1.1.7"
+      }
+    },
+    "node_modules/@ethersproject/solidity": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/solidity/-/solidity-5.8.0.tgz",
+      "integrity": "sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/sha2": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/strings": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/strings/-/strings-5.8.0.tgz",
+      "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/transactions": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/transactions/-/transactions-5.8.0.tgz",
+      "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/rlp": "^5.8.0",
+        "@ethersproject/signing-key": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/units": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/units/-/units-5.8.0.tgz",
+      "integrity": "sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/wallet": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/wallet/-/wallet-5.8.0.tgz",
+      "integrity": "sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-provider": "^5.8.0",
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/hash": "^5.8.0",
+        "@ethersproject/hdnode": "^5.8.0",
+        "@ethersproject/json-wallets": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/random": "^5.8.0",
+        "@ethersproject/signing-key": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "@ethersproject/wordlists": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/web": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/web/-/web-5.8.0.tgz",
+      "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/base64": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/wordlists": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/wordlists/-/wordlists-5.8.0.tgz",
+      "integrity": "sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/hash": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@noble/ciphers": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/@noble/ciphers/-/ciphers-1.3.0.tgz",
+      "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@noble/curves": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmmirror.com/@noble/curves/-/curves-1.9.1.tgz",
+      "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "1.8.0"
+      },
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@noble/hashes": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.8.0.tgz",
+      "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@polymarket/builder-signing-sdk": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmmirror.com/@polymarket/builder-signing-sdk/-/builder-signing-sdk-0.0.8.tgz",
+      "integrity": "sha512-rZLCFxEdYahl5FiJmhe22RDXysS1ibFJlWz4NT0s3itJRYq3XJzXXHXEZkAQplU+nIS1IlbbKjA4zDQaeCyYtg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "^18.7.18",
+        "axios": "^1.12.2",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@polymarket/clob-client": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmmirror.com/@polymarket/clob-client/-/clob-client-5.2.1.tgz",
+      "integrity": "sha512-ZAYzCiPbAATcocUg2Uh+ZuktpQcyMZXyRWvV4w0mCXQy46qHYCgLoQBWEWDFEcQpkteqf9cEngOcr31tjMuuBg==",
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/providers": "^5.7.2",
+        "@ethersproject/units": "^5.7.0",
+        "@ethersproject/wallet": "^5.7.0",
+        "@polymarket/builder-signing-sdk": "^0.0.8",
+        "@polymarket/order-utils": "^3.0.1",
+        "axios": "^1.0.0",
+        "browser-or-node": "^2.1.1",
+        "ethers": "^5.7.1",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@polymarket/order-utils": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/@polymarket/order-utils/-/order-utils-3.0.1.tgz",
+      "integrity": "sha512-XVcVladfGtC/VmboMkcszqYs82rvath/0XFWqzIFfq8O4atVOU8ykPOGJ2ZfodBPcXETzL+2u1rcepLMLKu9AQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/providers": "^5.7.2",
+        "@ethersproject/wallet": "^5.7.0",
+        "ethers": "^5.7.1",
+        "tslib": "^2.4.0",
+        "viem": "^2.31.4"
+      },
+      "engines": {
+        "node": ">=20.10",
+        "yarn": ">=1"
+      }
+    },
+    "node_modules/@scure/base": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmmirror.com/@scure/base/-/base-1.2.6.tgz",
+      "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@scure/bip32": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmmirror.com/@scure/bip32/-/bip32-1.7.0.tgz",
+      "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/curves": "~1.9.0",
+        "@noble/hashes": "~1.8.0",
+        "@scure/base": "~1.2.5"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@scure/bip39": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/@scure/bip39/-/bip39-1.6.0.tgz",
+      "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "~1.8.0",
+        "@scure/base": "~1.2.5"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "18.19.130",
+      "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.130.tgz",
+      "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "node_modules/abitype": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/abitype/-/abitype-1.2.3.tgz",
+      "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/wevm"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.4",
+        "zod": "^3.22.0 || ^4.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "zod": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/aes-js": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/aes-js/-/aes-js-3.0.0.tgz",
+      "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==",
+      "license": "MIT"
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.13.3",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.3.tgz",
+      "integrity": "sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.4",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/bech32": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/bech32/-/bech32-1.1.4.tgz",
+      "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==",
+      "license": "MIT"
+    },
+    "node_modules/bn.js": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-5.2.2.tgz",
+      "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
+      "license": "MIT"
+    },
+    "node_modules/brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
+      "license": "MIT"
+    },
+    "node_modules/browser-or-node": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/browser-or-node/-/browser-or-node-2.1.1.tgz",
+      "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==",
+      "license": "MIT"
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.19",
+      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz",
+      "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/elliptic": {
+      "version": "6.6.1",
+      "resolved": "https://registry.npmmirror.com/elliptic/-/elliptic-6.6.1.tgz",
+      "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
+      "license": "MIT",
+      "dependencies": {
+        "bn.js": "^4.11.9",
+        "brorand": "^1.1.0",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.1",
+        "inherits": "^2.0.4",
+        "minimalistic-assert": "^1.0.1",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "node_modules/elliptic/node_modules/bn.js": {
+      "version": "4.12.2",
+      "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.2.tgz",
+      "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
+      "license": "MIT"
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ethers": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/ethers/-/ethers-5.8.0.tgz",
+      "integrity": "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abi": "5.8.0",
+        "@ethersproject/abstract-provider": "5.8.0",
+        "@ethersproject/abstract-signer": "5.8.0",
+        "@ethersproject/address": "5.8.0",
+        "@ethersproject/base64": "5.8.0",
+        "@ethersproject/basex": "5.8.0",
+        "@ethersproject/bignumber": "5.8.0",
+        "@ethersproject/bytes": "5.8.0",
+        "@ethersproject/constants": "5.8.0",
+        "@ethersproject/contracts": "5.8.0",
+        "@ethersproject/hash": "5.8.0",
+        "@ethersproject/hdnode": "5.8.0",
+        "@ethersproject/json-wallets": "5.8.0",
+        "@ethersproject/keccak256": "5.8.0",
+        "@ethersproject/logger": "5.8.0",
+        "@ethersproject/networks": "5.8.0",
+        "@ethersproject/pbkdf2": "5.8.0",
+        "@ethersproject/properties": "5.8.0",
+        "@ethersproject/providers": "5.8.0",
+        "@ethersproject/random": "5.8.0",
+        "@ethersproject/rlp": "5.8.0",
+        "@ethersproject/sha2": "5.8.0",
+        "@ethersproject/signing-key": "5.8.0",
+        "@ethersproject/solidity": "5.8.0",
+        "@ethersproject/strings": "5.8.0",
+        "@ethersproject/transactions": "5.8.0",
+        "@ethersproject/units": "5.8.0",
+        "@ethersproject/wallet": "5.8.0",
+        "@ethersproject/web": "5.8.0",
+        "@ethersproject/wordlists": "5.8.0"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz",
+      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+      "license": "MIT"
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hash.js": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmmirror.com/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
+      "license": "MIT",
+      "dependencies": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/isows": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/isows/-/isows-1.0.7.tgz",
+      "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "peerDependencies": {
+        "ws": "*"
+      }
+    },
+    "node_modules/js-sha3": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmmirror.com/js-sha3/-/js-sha3-0.8.0.tgz",
+      "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==",
+      "license": "MIT"
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "license": "ISC"
+    },
+    "node_modules/minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
+      "license": "MIT"
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/ox": {
+      "version": "0.11.3",
+      "resolved": "https://registry.npmmirror.com/ox/-/ox-0.11.3.tgz",
+      "integrity": "sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@adraffy/ens-normalize": "^1.11.0",
+        "@noble/ciphers": "^1.3.0",
+        "@noble/curves": "1.9.1",
+        "@noble/hashes": "^1.8.0",
+        "@scure/bip32": "^1.7.0",
+        "@scure/bip39": "^1.6.0",
+        "abitype": "^1.2.3",
+        "eventemitter3": "5.0.1"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
+    "node_modules/qs": {
+      "version": "6.14.1",
+      "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.1.tgz",
+      "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/scrypt-js": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/scrypt-js/-/scrypt-js-3.0.1.tgz",
+      "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==",
+      "license": "MIT"
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    },
+    "node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+      "license": "MIT"
+    },
+    "node_modules/viem": {
+      "version": "2.45.1",
+      "resolved": "https://registry.npmmirror.com/viem/-/viem-2.45.1.tgz",
+      "integrity": "sha512-LN6Pp7vSfv50LgwhkfSbIXftAM5J89lP9x8TeDa8QM7o41IxlHrDh0F9X+FfnCWtsz11pEVV5sn+yBUoOHNqYA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@noble/curves": "1.9.1",
+        "@noble/hashes": "1.8.0",
+        "@scure/bip32": "1.7.0",
+        "@scure/bip39": "1.6.0",
+        "abitype": "1.2.3",
+        "isows": "1.0.7",
+        "ox": "0.11.3",
+        "ws": "8.18.3"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.4"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/viem/node_modules/ws": {
+      "version": "8.18.3",
+      "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz",
+      "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.19.0",
+      "resolved": "https://registry.npmmirror.com/ws/-/ws-8.19.0.tgz",
+      "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    }
+  }
+}

+ 21 - 0
polymarket/package.json

@@ -0,0 +1,21 @@
+{
+  "name": "polymarket",
+  "version": "1.0.0",
+  "description": "",
+  "main": "main.js",
+  "type": "module",
+  "scripts": {
+    "dev": "nodemon --ignore data/ --ignore cache/ --ignore node_modules/ --inspect=9227 main.js"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "@polymarket/clob-client": "^5.2.1",
+    "axios": "^1.13.3",
+    "dayjs": "^1.11.19",
+    "ethers": "^5.8.0",
+    "https-proxy-agent": "^7.0.6",
+    "qs": "^6.14.1",
+    "ws": "^8.19.0"
+  }
+}

+ 46 - 0
server/libs/cache.js

@@ -0,0 +1,46 @@
+import fs from 'fs';
+import path from 'path';
+
+export const getData = (file) => {
+  let data = null;
+
+  if (fs.existsSync(file)) {
+    const arrayBuffer = fs.readFileSync(file);
+
+    try {
+      data = JSON.parse(arrayBuffer.toString());
+    }
+    catch (e) {}
+  }
+
+  return Promise.resolve(data);
+}
+
+export const setData = (file, data, indent = 2) => {
+  return new Promise((resolve, reject) => {
+
+    if (typeof (data) != 'string') {
+      try {
+        data = JSON.stringify(data, null, indent);
+      }
+      catch (error) {
+        reject(error);
+      }
+    }
+
+    const directoryPath = path.dirname(file);
+    if(!fs.existsSync(directoryPath)) {
+      fs.mkdirSync(directoryPath, { recursive: true });
+    }
+
+    try {
+      fs.writeFileSync(file, data);
+      resolve();
+    }
+    catch (error) {
+      reject(error);
+    }
+  });
+}
+
+export default { getData, setData };

+ 79 - 0
server/libs/getDateInTimezone.js

@@ -0,0 +1,79 @@
+/**
+ * 将时区偏移小时转换为 ±HHMM 格式
+ * @param {number|string} offset 如 8, -4, "+8", "-04"
+ * @returns {string} 如 "+0800", "-0400"
+ */
+const formatTimezoneOffset = (offset) => {
+  const hours = Number(offset);
+  if (Number.isNaN(hours)) {
+    throw new Error('Invalid timezone offset');
+  }
+
+  const sign = hours >= 0 ? '+' : '-';
+  const hh = String(Math.abs(hours)).padStart(2, '0');
+
+  return `${sign}${hh}00`;
+}
+
+
+/**
+ * 判断日期是否无效
+ * @param {Date} date
+ * @returns {boolean}
+ */
+const isInvalidDate = (date) => {
+  return date instanceof Date && Number.isNaN(date.getTime());
+}
+
+
+/**
+ * 获取指定时区当前日期或时间
+ * @param {number} offset - 时区相对 UTC 的偏移(例如:-4 表示 GMT-4)
+ * @param {number} timestamp - 时间戳(可选)
+ * @param {boolean} [withTime=false] - 是否返回完整时间(默认只返回日期)
+ * @returns {string} 格式化的日期或时间字符串
+ */
+const getDateInTimezone = (offset, timestamp, withTime=false) => {
+
+  offset = Number(offset);
+  if (Number.isNaN(offset)) {
+    throw new Error('Invalid timezone offset');
+  }
+
+  if (typeof(timestamp) === 'undefined') {
+    timestamp = Date.now();
+  }
+  else if (typeof(timestamp) === 'boolean') {
+    withTime = timestamp;
+    timestamp = Date.now();
+  }
+  else if (typeof(timestamp) === 'string') {
+    const date = new Date(timestamp);
+    if (isInvalidDate(date)) {
+      throw new Error('Invalid timestamp');
+    }
+    timestamp = date.getTime();
+  }
+  else if (typeof(timestamp) !== 'number') {
+    throw new Error('Invalid timestamp');
+  }
+
+  const nowUTC = new Date(timestamp);
+  const targetTime = new Date(nowUTC.getTime() + offset * 60 * 60 * 1000);
+
+  const year = targetTime.getUTCFullYear();
+  const month = String(targetTime.getUTCMonth() + 1).padStart(2, '0');
+  const day = String(targetTime.getUTCDate()).padStart(2, '0');
+
+  if (!withTime) {
+    return `${year}-${month}-${day}`;
+  }
+
+  const hours = String(targetTime.getUTCHours()).padStart(2, '0');
+  const minutes = String(targetTime.getUTCMinutes()).padStart(2, '0');
+  const seconds = String(targetTime.getUTCSeconds()).padStart(2, '0');
+
+  return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${formatTimezoneOffset(offset)}`;
+}
+
+export default getDateInTimezone;

+ 27 - 0
server/libs/getTranslation.js

@@ -0,0 +1,27 @@
+import localesData from "../state/locales.js";
+import translateText from "../models/Translation.js";
+
+const getTranslation = async (names) => {
+  const translated = {};
+  const untranslated = [];
+  const localesMap = await localesData.get(names);
+  Object.keys(localesMap).forEach(key => {
+    if (localesMap[key]) {
+      translated[key] = localesMap[key];
+    }
+    else {
+      untranslated.push(key);
+    }
+  });
+
+  if (untranslated.length === 0) {
+    return translated;
+  }
+
+  const translations = await translateText(untranslated);
+  await localesData.set(translations);
+  Object.assign(translated, translations);
+  return translated;
+}
+
+export default getTranslation;

+ 45 - 0
server/libs/logs.js

@@ -0,0 +1,45 @@
+import dayjs from 'dayjs';
+
+export default class Logs {
+
+  static out(...args) {
+    const timeString = dayjs().format('YYYY-MM-DD HH:mm:ss.SSS');
+    if (typeof args[0] === 'string' && args[0].includes('%')) {
+      args[0] = `[${timeString}] ` + args[0];
+    }
+    else {
+      args.unshift(`[${timeString}]`);
+    }
+    console.log(...args);
+  }
+
+  static err(...args) {
+    const timeString = dayjs().format('YYYY-MM-DD HH:mm:ss.SSS');
+    if (typeof args[0] === 'string' && args[0].includes('%')) {
+      args[0] = `[${timeString}] ` + args[0];
+    }
+    else {
+      args.unshift(`[${timeString}]`);
+    }
+    console.error(...args);
+  }
+
+  static outDev(...args) {
+    if (process.env.NODE_ENV == 'development') {
+      this.out(...args);
+    }
+  }
+
+  static errDev(...args) {
+    if (process.env.NODE_ENV == 'development') {
+      this.err(...args);
+    }
+  }
+
+  static outLine(string) {
+    process.stdout.write("\u001b[1A");
+    process.stdout.write("\u001b[2K");
+    this.out(string);
+  }
+
+}

+ 84 - 0
server/libs/processData.js

@@ -0,0 +1,84 @@
+import { randomUUID } from "crypto";
+
+import Logs from "./logs.js";
+
+class ProcessData {
+  #process;
+  #name;
+  #request = {
+    callbacks: {},
+    functions: {}
+  };
+  #response = {
+    functions: {}
+  };
+
+  constructor(processInstance = process, name = 'process', showStdout = false) {
+    this.#process = processInstance;
+    this.#name = name;
+    this.#process.on('message', (message) => {
+      this.#handleMessage(message);
+    });
+    this.#process.stdout?.on('data', data => {
+      if (showStdout) {
+        Logs.out('%s stdout: ↩︎', name);
+        console.log(data.toString());
+      }
+    });
+    this.#process.stderr?.on('data', data => {
+      Logs.out('%s stderr', name, data.toString());
+    });
+  }
+
+  get(type, params) {
+    const id = randomUUID();
+    this.#process.send({ method: 'get', type, id, params });
+    return new Promise(resolve => {
+      this.#request.callbacks[id] = resolve;
+    });
+  }
+
+  registerResponse(type, func) {
+    this.#response.functions[type] = func;
+  }
+
+  #responseData(type, params, id) {
+    const func = this.#response.functions[type];
+    func?.(params).then(data => {
+      this.#process.send({ type: 'response', data, id });
+    });
+  }
+
+  post(type, data) {
+    this.#process.send({ method: 'post', type, data });
+  }
+
+  registerRequest(type, func) {
+    this.#request.functions[type] = func;
+  }
+
+  #requestData(type, data) {
+    const func = this.#request.functions[type];
+    func?.(data);
+  }
+
+  #handleMessage(message) {
+    const { callbacks } = this.#request;
+    const { method, type, data, error, id, params } = message;
+    if (error) {
+      console.error(error);
+    }
+    if (method == 'get' && id) {
+      this.#responseData(type, params, id);
+    }
+    else if (type == 'response' && id && callbacks[id]) {
+      callbacks[id](data);
+      delete callbacks[id];
+    }
+    else if (method == 'post' && type) {
+      this.#requestData(type, data);
+    }
+  }
+}
+
+export default ProcessData;

+ 127 - 0
server/libs/wsClientData.js

@@ -0,0 +1,127 @@
+import { randomUUID } from "crypto";
+
+import Logs from "./logs.js";
+
+/**
+ * 解析推送过来的消息
+ * @param {*} data
+ * @returns {object}
+ */
+const parseMessage = (data) => {
+  let message;
+  try {
+    message = JSON.parse(data);
+  }
+  catch(e) {
+    message = { text: data };
+  }
+  return message;
+}
+
+class WebSocketClient {
+  #wsClient;
+  constructor(wsClient) {
+    this.#wsClient = wsClient;
+  }
+  on(type, callback) {
+    if (type == 'message') {
+      this.#wsClient.on('message', (event) => {
+        const data = parseMessage(event.data);
+        callback(data);
+      });
+    }
+    else {
+      this.#wsClient.on(type, callback);
+    }
+  }
+  send(message) {
+    this.#wsClient.send(JSON.stringify(message));
+  }
+  close() {
+    this.#wsClient.close();
+  }
+}
+
+class WsClientData {
+  #wsClient;
+  #name;
+  #request = {
+    callbacks: {},
+    functions: {}
+  };
+  #response = {
+    functions: {}
+  };
+
+  constructor(wsClient, name='wsClient') {
+    this.#wsClient = wsClient.constructor.name === 'WebSocket' ? new WebSocketClient(wsClient) : wsClient;
+    this.#name = name;
+    this.#wsClient.on('message', (message) => {
+      this.#handleMessage(message);
+    });
+    this.#wsClient.on('error', (error) => {
+      Logs.err('wsClient error', error);
+    });
+    this.#wsClient.on('close', () => {
+      Logs.outDev('wsClient closed');
+    });
+  }
+
+  get(type, params) {
+    return new Promise((resolve, reject) => {
+      const id = randomUUID();
+      try {
+        this.#wsClient.send({ method: 'get', type, id, params });
+        this.#request.callbacks[id] = resolve;
+      }
+      catch (error) {
+        delete this.#request.callbacks[id];
+        reject(error);
+      }
+    });
+  }
+
+  registerResponse(type, func) {
+    this.#response.functions[type] = func;
+  }
+
+  #responseData(type, params, id) {
+    const func = this.#response.functions[type];
+    func?.(params).then(data => {
+      this.#wsClient.send({ type: 'response', data, id });
+    });
+  }
+
+  post(type, data) {
+    this.#wsClient.send({ method: 'post', type, data });
+  }
+
+  registerRequest(type, func) {
+    this.#request.functions[type] = func;
+  }
+
+  #requestData(type, data) {
+    const func = this.#request.functions[type];
+    func?.(data);
+  }
+
+  #handleMessage(message) {
+    const { callbacks } = this.#request;
+    const { method, type, data, error, id, params } = message;
+    if (error) {
+      console.error(error);
+    }
+    if (method == 'get' && id) {
+      this.#responseData(type, params, id);
+    }
+    else if (type == 'response' && id && callbacks[id]) {
+      callbacks[id](data);
+      delete callbacks[id];
+    }
+    else if (method == 'post' && type) {
+      this.#requestData(type, data);
+    }
+  }
+}
+
+export default WsClientData;

+ 84 - 0
server/main.js

@@ -0,0 +1,84 @@
+import express from 'express';
+import expressWs from 'express-ws';
+import dotenv from 'dotenv';
+import cookieParser from 'cookie-parser';
+import Logs from './libs/logs.js';
+import gamesRoutes from './routes/games.js';
+import localesRoutes from './routes/locales.js';
+import platformsRoutes from './routes/platforms.js';
+
+const app = express();
+const wsInstance = expressWs(app);
+
+dotenv.config();
+
+// 添加 CORS 支持.env
+app.use((req, res, next) => {
+  res.header('Access-Control-Allow-Origin', '*');
+  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
+  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+
+  if (req.method === 'OPTIONS') {
+    return res.sendStatus(200);
+  }
+  next();
+});
+
+// 中间件
+app.use(express.json({ limit: '10mb' }));
+app.use(cookieParser());
+
+app.use((req, res, next) => {
+  res.badRequest = (data, msg) => {
+    if (!msg && typeof data === 'string') {
+      msg = data;
+      data = undefined;
+    }
+    return res.status(400).json({ statusCode: 400, code: -1, message: msg ?? 'Bad Request', data });
+  }
+  res.unauthorized = (data,msg) => {
+    if (!msg && typeof data === 'string') {
+      msg = data;
+      data = undefined;
+    }
+    return res.status(401).json({ statusCode: 401, code: -1,  message: msg ?? 'Unauthorized', data });
+  }
+    res.notFound = (data,msg) => {
+    if (!msg && typeof data === 'string') {
+      msg = data;
+      data = undefined;
+    }
+    return res.status(404).json({ statusCode: 404, code: -1,  message: msg ?? 'Not Found', data });
+  }
+  res.serverError = (data,msg) => {
+    if (!msg && typeof data === 'string') {
+      msg = data;
+      data = undefined;
+    }
+    return res.status(500).json({ statusCode: 500, code: -1,  message: msg ?? 'Internal Server Error', data });
+  }
+  res.sendSuccess = (data, msg) => {
+    const response = { statusCode: 200, code: 0,  message: msg ?? 'OK' }
+    if (data) {
+      response.data = data;
+    }
+    return res.status(200).json(response);
+  }
+  res.sendError = (err) => {
+    if (err.cause == 400) {
+      return res.badRequest(err.data, err.message);
+    }
+    else {
+      return res.serverError(err.data, err.message);
+    }
+  }
+  next();
+});
+
+app.use('/api/games', gamesRoutes);
+app.use('/api/locales', localesRoutes);
+app.use('/api/platforms', platformsRoutes);
+
+// 启动服务
+const PORT = process.env.PORT || 9020;
+app.listen(PORT, () => Logs.out(`Server running on port ${PORT}`));

+ 221 - 0
server/models/Games.js

@@ -0,0 +1,221 @@
+import GetTranslation from "../libs/getTranslation.js";
+import Store from "../state/store.js";
+
+import { getPlatformIorsDetailInfo, getSolutionByLatestIors, getSoulutionBetResult } from "./Markets.js";
+
+export const getLeagues = async () => {
+  const { polymarket, pinnacle } = Store.get('leagues') ?? { polymarket: [], pinnacle: [] };
+  const polymarketNames = polymarket.map(item => item.name);
+  const translatedNames = await GetTranslation(polymarketNames);
+  const newPolymarket = polymarket.map(item => {
+    const { name } = item;
+    const localesName = translatedNames[name] ?? name;
+    return { ...item, localesName };
+  });
+  return { polymarket: newPolymarket, pinnacle };
+}
+
+export const setLeaguesRelation = async (relation) => {
+  const { id, platforms } = relation;
+  if (!id || !platforms) {
+    return Promise.reject(new Error('invalid request', { cause: 400 }));
+  }
+  const storeRelations = Store.get('leaguesRelations') ?? {};
+  if (storeRelations[id]) {
+    return Promise.reject(new Error('relation already exists', { cause: 400 }));
+  }
+  storeRelations[id] = relation;
+  Store.set('leaguesRelations', storeRelations);
+  return Promise.resolve();
+};
+
+export const removeLeaguesRelation = async (id) => {
+  if (!id) {
+    return Promise.reject(new Error('invalid request', { cause: 400 }));
+  }
+  const storeRelations = Store.get('leaguesRelations') ?? {};
+  if (!storeRelations[id]) {
+    return Promise.reject(new Error('relation not found', { cause: 400 }));
+  }
+  delete storeRelations[id];
+  Store.set('leaguesRelations', storeRelations);
+  return Promise.resolve();
+};
+
+export const getLeaguesRelations = async () => {
+  const storeRelations = Object.values(Store.get('leaguesRelations') ?? {});
+  const polymarketNames = storeRelations.map(item => item.platforms.polymarket.name);
+  const translatedNames = await GetTranslation(polymarketNames);
+  const newRelations = storeRelations.map(item => {
+    const { platforms: { polymarket, pinnacle } } = item;
+    const { name } = polymarket;
+    const localesName = translatedNames[name] ?? name;
+    return { ...item, platforms: { polymarket: { ...polymarket, localesName }, pinnacle } };
+  });
+  return Promise.resolve(newRelations);
+};
+
+export const getGames = async () => {
+  const { polymarket, pinnacle } = Store.get('games') ?? { polymarket: [], pinnacle: [] };
+  const polymarketNames = [ ...new Set(polymarket.map(item => [item.teamHomeName, item.teamAwayName, item.leagueName]).flat()) ];
+  const translatedNames = await GetTranslation(polymarketNames);
+  const newPolymarket = polymarket.map(item => {
+    const { leagueName, teamHomeName, teamAwayName } = item;
+    const localesTeamHomeName = translatedNames[teamHomeName] ?? teamHomeName;
+    const localesTeamAwayName = translatedNames[teamAwayName] ?? teamAwayName;
+    const localesLeagueName = translatedNames[leagueName] ?? leagueName;
+    return { ...item, localesTeamHomeName, localesTeamAwayName, localesLeagueName };
+  }).filter(item => {
+    const { timestamp } = item;
+    const now = Date.now();
+    return (timestamp + 1000 * 60 * 60 * 2) > now;
+  }).sort((a, b) => a.timestamp - b.timestamp);
+
+  return { polymarket: newPolymarket, pinnacle };
+}
+
+export const setGamesRelation = async (relation) => {
+  const { id, platforms, timestamp } = relation;
+  if (!id || !platforms || !timestamp) {
+    return Promise.reject(new Error('invalid request', { cause: 400 }));
+  }
+  const storeRelations = Store.get('gamesRelations') ?? {};
+  if (storeRelations[id]) {
+    return Promise.reject(new Error('relation already exists', { cause: 400 }));
+  }
+  storeRelations[id] = relation;
+  Store.set('gamesRelations', storeRelations);
+  return Promise.resolve();
+}
+
+export const removeGamesRelation = async (id) => {
+  if (!id) {
+    return Promise.reject(new Error('invalid request', { cause: 400 }));
+  }
+  const storeRelations = Store.get('gamesRelations') ?? {};
+  if (!storeRelations[id]) {
+    return Promise.reject(new Error('relation not found', { cause: 400 }));
+  }
+  delete storeRelations[id];
+  Store.set('gamesRelations', storeRelations);
+  return Promise.resolve();
+}
+
+export const getGamesRelations = async () => {
+  const storeRelations = Object.values(Store.get('gamesRelations') ?? {});
+  const polymarketNames = [ ...new Set(storeRelations.map(item => {
+    const { teamHomeName, teamAwayName, leagueName } = item.platforms.polymarket;
+    return [teamHomeName, teamAwayName, leagueName];
+  }).flat()) ];
+  const translatedNames = await GetTranslation(polymarketNames);
+  const newRelations = storeRelations.map(item => {
+    const { platforms: { polymarket, pinnacle } } = item;
+    const { teamHomeName, teamAwayName, leagueName } = polymarket;
+    const localesTeamHomeName = translatedNames[teamHomeName] ?? teamHomeName;
+    const localesTeamAwayName = translatedNames[teamAwayName] ?? teamAwayName;
+    const localesLeagueName = translatedNames[leagueName] ?? leagueName;
+    return { ...item, platforms: { polymarket: { ...polymarket, localesTeamHomeName, localesTeamAwayName, localesLeagueName }, pinnacle } };
+  }).sort((a, b) => a.timestamp - b.timestamp);
+  return Promise.resolve(newRelations);
+}
+
+/**
+ * 获取解决方案
+ * @param {*} param0
+ * @returns
+ */
+export const getSolutions = async ({ min_profit_rate = 0 } = {}) => {
+  const solutions = Store.get('solutions') ?? [];
+  const solutionsList = solutions.filter(solution => {
+    const { sol: { win_profit_rate } } = solution;
+    return win_profit_rate >= min_profit_rate;
+  }).sort((a, b) => {
+    return b.sol.win_profit_rate - a.sol.win_profit_rate;
+  });
+  const gamesRelations = Store.get('gamesRelations') ?? {};
+  const selectedRelations = {};
+  solutionsList.forEach(solution => {
+    const { info: { id }, ...solutionRest } = solution;
+    if (!gamesRelations[id]) {
+      return;
+    }
+    if (!selectedRelations[id]) {
+      selectedRelations[id] = { ...gamesRelations[id] };
+    }
+    if (!selectedRelations[id]['solutions']) {
+      selectedRelations[id]['solutions'] = [];
+    }
+    selectedRelations[id]['solutions'].push(solutionRest);
+  });
+  const relationsList = Object.values(selectedRelations).sort((a, b) => {
+    return b.solutions[0].sol.win_profit_rate - a.solutions[0].sol.win_profit_rate;
+  });
+  return Promise.resolve(relationsList);
+}
+
+/**
+ * 获取策略对应的盘口信息
+ */
+export const getSolutionIorsInfo = async (sid) => {
+  const solution = Store.get('solutions')?.find(item => item.sid == sid);
+  if (!solution) {
+    return Promise.reject(new Error('solution not found', { cause: 400 }));
+  }
+  const { info: { id }, cpr, sol: { cross_type } } = solution;
+  const gamesRelations = Store.get('gamesRelations') ?? {};
+  const gameRelation = gamesRelations[id];
+  if (!gameRelation) {
+    return Promise.reject(new Error('game relation not found', { cause: 400 }));
+  }
+  const { platforms: { polymarket, pinnacle } } = gameRelation;
+  const idMap = { polymarket: polymarket.id, pinnacle: pinnacle.id };
+  const iorsInfo = await Promise.all(cpr.map(item => {
+    const { k, p } = item;
+    return getPlatformIorsDetailInfo(k, p, idMap[p]);
+  }));
+
+  return { cpr, iorsInfo, cross_type };
+  // const accountBalance = await getAccountBalance();
+  // const solutionInfo = getSolutionByLatestIors(iorsInfo, cross_type);
+  // return { cpr, iorsInfo, ...solutionInfo };
+}
+
+/**
+ * 根据策略下注
+ */
+export const betSolution = async (sid, stake=0) => {
+  const solutionIorsInfo = await getSolutionIorsInfo(sid);
+  const { iorsInfo, cross_type } = solutionIorsInfo;
+  const solutionInfo = getSolutionByLatestIors(iorsInfo, cross_type);
+  if (solutionInfo?.error) {
+    const error = new Error(solutionInfo.error, { cause: 400 });
+    error.data = solutionInfo.data;
+    return Promise.reject(error);
+  }
+  const betResult = await getSoulutionBetResult({ ...solutionIorsInfo, ...solutionInfo, stake });
+  return { betResult, ...solutionIorsInfo, ...solutionInfo };
+}
+
+/**
+ * 清理过期关系
+ */
+const cleanGamesRelations = () => {
+  const now = Date.now();
+  const storeRelations = Store.get('gamesRelations') ?? [];
+  Object.keys(storeRelations).forEach(key => {
+    const relation = storeRelations[key];
+    const { timestamp } = relation;
+    if ((timestamp + 1000 * 60 * 60 * 2) < now) {
+      delete storeRelations[key];
+    }
+  });
+  Store.set('gamesRelations', storeRelations);
+}
+
+setInterval(cleanGamesRelations, 1000 * 60);
+
+export default {
+  getLeagues, setLeaguesRelation, removeLeaguesRelation, getLeaguesRelations,
+  getGames, setGamesRelation, removeGamesRelation, getGamesRelations,
+  getSolutions, getSolutionIorsInfo, betSolution,
+};

+ 15 - 0
server/models/Locales.js

@@ -0,0 +1,15 @@
+import localesData from "../state/locales.js";
+
+export const getLocales = async () => {
+  return localesData.get();
+}
+
+export const setLocales = async (locales) => {
+  return localesData.set(locales);
+}
+
+export const removeLocales = async (keys) => {
+  return localesData.remove(keys);
+}
+
+export default { getLocales, setLocales, removeLocales };

+ 446 - 0
server/models/Markets.js

@@ -0,0 +1,446 @@
+import path from "path";
+import { fileURLToPath } from "url";
+
+import { getData } from "../libs/cache.js";
+import Logs from "../libs/logs.js";
+
+import eventSolutions from '../triangle/eventSolutions.js';
+
+import { getOrderBook, placeOrder as polymarketPlaceOrder } from "../../polymarket/libs/polymarketClient.js";
+import { getLineInfo, placeOrder as pinnaclePlaceOrder } from "../../pinnacle/libs/pinnacleClient.js";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const polymarketMarketsCacheFile = path.join(__dirname, "../../polymarket/cache/polymarketMarketsCache.json");
+const pinnacleGamesCacheFile = path.join(__dirname, "../../pinnacle/cache/pinnacleGamesCache.json");
+
+/**
+ * USDC汇率
+ */
+const USDC_RATE = 6.9;
+
+/**
+ * 最小盈利率
+ */
+const MIN_PROFIT_RATE = -1;
+
+/**
+ * 精确浮点数字
+ * @param {number} number
+ * @param {number} x
+ * @returns {number}
+ */
+const fixFloat = (number, x=3) => {
+  return parseFloat(number.toFixed(x));
+}
+
+/**
+ * 依次执行任务
+ */
+const runSequentially = async (tasks) => {
+  return new Promise(async (resolve, reject) => {
+    const results = [];
+    let isError = false;
+    for (const task of tasks) {
+      // task 必须是一个「返回 Promise 的函数」
+      const res = await task().catch(err => {
+        err.results = results;
+        isError = true;
+        reject(err);
+      });
+      if (isError) {
+        break;
+      }
+      else if (res) {
+        results.push(res);
+      }
+    }
+    if (isError) {
+      return;
+    }
+    resolve(results);
+  })
+}
+
+/**
+ * 计算符合比例的最大和最小数组
+ */
+const findMaxMinGroup = (ratios, minVals, maxVals) => {
+  const n = ratios.length;
+
+  // 1. 计算比例总和 & 归一化比例
+  const totalRatio = ratios.reduce((a, b) => a + b, 0);
+  const proportions = ratios.map(r => r / totalRatio);
+
+  // 2. 计算每个位置允许的最大/最小倍数
+  let maxPossibleScale = Infinity;
+  let minPossibleScale = 0;   // 如果允许0,通常从0开始
+
+  for (let i = 0; i < n; i++) {
+    // 上限约束
+    if (proportions[i] > 0) {
+      maxPossibleScale = Math.min(maxPossibleScale, maxVals[i] / proportions[i]);
+    }
+    // 下限约束(如果 minVals[i] > 0 才有意义)
+    if (proportions[i] > 0 && minVals[i] > 0) {
+      minPossibleScale = Math.max(minPossibleScale, minVals[i] / proportions[i]);
+    }
+  }
+
+  // 3. 最终取值
+  const maxGroup = proportions.map(p => p * maxPossibleScale);
+  const minGroup = proportions.map(p => p * Math.max(minPossibleScale, 0));
+
+  return {
+    maxGroup: maxGroup.map(v => fixFloat(v)), // 可控制精度
+    minGroup: minGroup.map(v => fixFloat(v)),
+    proportions: proportions.map(v => fixFloat(v)),
+    scaleForMax: fixFloat(maxPossibleScale),
+    scaleForMin: fixFloat(minPossibleScale)
+  };
+}
+
+/**
+ * 解析盘口信息
+ */
+const parseRatio = (ratioString) => {
+  if (!ratioString) {
+    return null;
+  }
+  return parseFloat(`${ratioString[0]}.${ratioString.slice(1)}`);
+}
+
+const parseIor = (ior) => {
+  const iorMatch = ior.match(/ior_(m|r|ou|wm|ot)([ao])?([hcn])?_?(\d+)?/);
+  if (!iorMatch) {
+    return null;
+  }
+  const [, type, action, side, ratio] = iorMatch;
+  return { type, action, side, ratio };
+}
+
+const getPolymarketIorInfo = async (ior, id) => {
+  const cacheData = await getData(polymarketMarketsCacheFile);
+  const marketsData = cacheData[id]?.marketsData;
+  if (!marketsData) {
+    Logs.outDev('polymarket markets data not found', id);
+    return null;
+  }
+  const iorOptions = parseIor(ior);
+  if (!iorOptions) {
+    Logs.outDev('polymarket ior options not found', ior);
+    return null;
+  }
+  const { type, action, side, ratio } = iorOptions;
+
+  let marketTypeData, outcomesSide;
+
+  if (type === 'm') {
+    const sideKey = side === 'h' ? 'Home' : side === 'c' ? 'Away' : 'Draw';
+    const sideAction = action === 'o' ? 'No' : 'Yes';
+    marketTypeData = marketsData.moneyline[sideKey];
+    outcomesSide = sideAction;
+  }
+
+  else if (type === 'r') {
+    const sideKey = side === 'h' ? 'Home' : side === 'c' ? 'Away' : '';
+    let ratioDirection = 1;
+    if (side === 'c' && action === 'a' || side === 'h' && !action) {
+      ratioDirection = -1;
+    }
+    const ratioValue = parseRatio(ratio) * ratioDirection;
+    const ratioKey = ratioValue > 0 ? `+${ratioValue}` : `${ratioValue}`;
+    marketTypeData = marketsData.spreads?.[ratioKey];
+    outcomesSide = sideKey;
+  }
+
+  else if (type === 'ou') {
+    const sideKey = side === 'c' ? 'Over' : side === 'h' ? 'Under' : '';
+    const ratioKey = parseRatio(ratio);
+    marketTypeData = marketsData.totals[ratioKey];
+    outcomesSide = sideKey;
+  }
+
+  const result = marketTypeData?.outcomes?.[outcomesSide];
+
+  if (!result) {
+    Logs.outDev('polymarket market type data not found', { ior, id, type, action, side, ratio, marketTypeData, outcomesSide });
+    return null;
+  }
+  return result;
+}
+
+const getPinnacleIorInfo = async (ior, id) => {
+  const cacheData = await getData(pinnacleGamesCacheFile);
+  const gamesData = cacheData[id];
+  if (!gamesData) {
+    Logs.outDev('pinnacle games data not found', id);
+    return null;
+  }
+  const iorOptions = parseIor(ior);
+  if (!iorOptions) {
+    Logs.outDev('pinnacle ior options not found', ior);
+    return null;
+  }
+  const { type, action, side, ratio } = iorOptions;
+
+  const { leagueId, id: eventId, home: homeTeamName, away: awayTeamName, periods={}, specials={}} = gamesData;
+
+  const straightData = periods.straight ?? {};
+  const { lineId: straightLineId, moneyline, spreads, totals } = straightData;
+  const { winningMargin, exactTotalGoals } = specials;
+
+  if (type === 'm' && moneyline) {
+    const sideKey = side === 'h' ? 'home' : side === 'c' ? 'away' : 'draw';
+    const team = side === 'h' ? 'TEAM1' : side === 'c' ? 'TEAM2' : 'DRAW';
+    const odds = moneyline[sideKey];
+    return { leagueId, eventId, betType: 'MONEYLINE', team, lineId: straightLineId, odds };
+  }
+
+  else if (type === 'r' && spreads) {
+    let ratioDirection = 1;
+    if (side === 'c' && action === 'a' || side === 'h' && !action) {
+      ratioDirection = -1;
+    }
+    const ratioKey = parseRatio(ratio) * ratioDirection;
+    const itemSpread = spreads.find(spread => spread.hdp == ratioKey);
+    if (!itemSpread) {
+      Logs.outDev('pinnacle item spread not found', id, type, action, side, ratio);
+      return null;
+    }
+    const { altLineId=null, home, away } = itemSpread;
+    const odds = side === 'h' ? home : away;
+    const team = side === 'h' ? 'TEAM1' : 'TEAM2';
+    const handicap = ratioKey * (side === 'h' ? 1 : -1);
+    return { leagueId, eventId, handicap, betType: 'SPREAD', team, lineId: straightLineId, altLineId, odds };
+  }
+
+  else if (type === 'ou' && totals) {
+    const ratioKey = parseRatio(ratio);
+    const itemTotal = totals.find(total => total.points == ratioKey);
+    if (!itemTotal) {
+      Logs.outDev('pinnacle item total not found', id, type, action, side, ratio);
+      return null;
+    }
+    const { altLineId=null, over, under } = itemTotal;
+    const odds = side === 'c' ? over : under;
+    const sideKey = side === 'c' ? 'OVER' : 'UNDER';
+    return { leagueId, eventId, handicap: ratioKey, betType: 'TOTAL_POINTS', side: sideKey, lineId: straightLineId, altLineId, odds };
+  }
+
+  else if (type === 'wm' && winningMargin) {
+    const ratioKey = parseRatio(ratio);
+    const { id: specialId } = winningMargin;
+    const wmName = side === 'h' ? `${homeTeamName} By ${ratioKey}` : side === 'c' ? `${awayTeamName} By ${ratioKey}` : '';
+    const wmItem = winningMargin.contestants.find(contestant => contestant.name == wmName);
+    if (!wmItem) {
+      Logs.outDev('pinnacle item winning margin not found', id, type, action, side, ratio);
+      return null;
+    }
+    const { id: contestantId, lineId, price } = wmItem;
+    return { leagueId, eventId, specialId, contestantId, lineId, odds: price };
+  }
+
+  else if (type === 'ot' && exactTotalGoals) {
+    const ratioKey = parseRatio(ratio);
+    const { id: specialId } = exactTotalGoals;
+    const otItem = exactTotalGoals.contestants.find(contestant => contestant.name == ratioKey);
+    if (!otItem) {
+      Logs.outDev('pinnacle item exact total goals not found', id, type, action, side, ratio);
+      return null;
+    }
+    const { id: contestantId, lineId, price } = otItem;
+    return { leagueId, eventId, specialId, contestantId, lineId, odds: price };
+  }
+
+  else {
+    Logs.outDev('pinnacle ior type not found', ior, id);
+    return null;
+  }
+}
+
+/**
+ * 获取平台盘口id信息
+ */
+export const getPlatformIorInfo = async (ior, platform, id) => {
+  const getInfo = {
+    polymarket() {
+      return getPolymarketIorInfo(ior, id);
+    },
+    pinnacle() {
+      return getPinnacleIorInfo(ior, id);
+    }
+  }
+  Logs.outDev('getPlatformIorInfo', { ior, platform, id });
+  return getInfo[platform]?.();
+}
+
+
+/**
+ * 获取polymarket盘口详细信息
+ */
+const getPolymarketIorDetailInfo = async (info) => {
+  const { id } = info;
+  return getOrderBook(id);
+}
+
+/**
+ * 获取pinnacle盘口详细信息
+ */
+const getPinnacleIorDetailInfo = async (info) => {
+  return getLineInfo(info);
+}
+
+/**
+ * 获取平台盘口详细信息
+ */
+export const getPlatformIorsDetailInfo = async (ior, platform, id) => {
+  const info = await getPlatformIorInfo(ior, platform, id);
+  if (!info) {
+    return Promise.reject(new Error('platform ior info not found', { cause: 400 }));
+  }
+  const getInfo = {
+    polymarket() {
+      return getPolymarketIorDetailInfo(info);
+    },
+    pinnacle() {
+      return getPinnacleIorDetailInfo(info);
+    }
+  }
+  return getInfo[platform]?.();
+}
+
+/**
+ * 根据最新赔率获取策略
+ */
+export const getSolutionByLatestIors = (iorsInfo, cross_type, retry=false) => {
+  const askIndex = +retry;
+  const iorsValues = iorsInfo.map(item => {
+    if (item.asks) {
+      const bestAsk = [...item.asks].sort((a, b) => a.price - b.price)[askIndex];
+      const value = fixFloat(1 / bestAsk.price, 3);
+      const maxStake = fixFloat(bestAsk.size * bestAsk.price * USDC_RATE);
+      const minStake = fixFloat(item.min_order_size * bestAsk.price * USDC_RATE);
+      return { value, maxStake, minStake, bestPrice: bestAsk.price };
+    }
+    else if (item.info) {
+      const value = item.info.price;
+      const maxStake = Math.floor(item.info.maxRiskStake);
+      const minStake = Math.ceil(item.info.minRiskStake*10);
+      return { value, maxStake, minStake };
+    }
+  });
+
+  const nullIndex = iorsValues.findIndex(item => item.value == null);
+
+  if (nullIndex >= 0) {
+    return { error: `IORS_NULL_VALUE_AT_INDEX_${nullIndex}_RETRY_${askIndex}`, data: iorsInfo };
+  }
+
+  if (iorsValues.length === 2) {
+    iorsValues.push({ value: 1, maxStake: 0, minStake: 0 });
+  }
+
+  const baseIndex = iorsValues.reduce((minIdx, cur, idx) => cur.value < iorsValues[minIdx].value ? idx : minIdx, 0);
+
+  const betInfo = {
+    cross_type,
+    base_index: baseIndex,
+    base_stake: 10000,
+    odds_side_a: fixFloat(iorsValues[0].value - 1),
+    odds_side_b: fixFloat(iorsValues[1].value - 1),
+    odds_side_c: fixFloat(iorsValues[2].value - 1),
+  };
+
+  const sol = eventSolutions(betInfo, true);
+  const { win_average, win_profit_rate, gold_side_a, gold_side_b, gold_side_c } = sol;
+
+  if (win_profit_rate < MIN_PROFIT_RATE) {
+    Logs.outDev('win_profit_rate is less than profit rate limit', sol, iorsValues, iorsInfo, cross_type);
+    return { error: `WIN_PROFIT_RATE_LESS_THAN_MIN_PROFIT_RATE_RETRY_${askIndex}`, data: { sol, iorsValues, iorsInfo } };
+  }
+
+  const goldRatios = [gold_side_a, gold_side_b];
+  if (gold_side_c) {
+    goldRatios.push(gold_side_c);
+  }
+  const minVals = iorsValues.map(item => item.minStake);
+  const maxVals = iorsValues.map(item => item.maxStake);
+
+  const stakeLimit = findMaxMinGroup(goldRatios, minVals, maxVals);
+  const { scaleForMax, scaleForMin } = stakeLimit;
+
+  if (scaleForMax < scaleForMin) {
+    Logs.outDev('scaleForMax is less than scaleForMin');
+    if (!retry) {
+      return getSolutionByLatestIors(iorsInfo, cross_type, true);
+    }
+    else {
+      return { error: `NO_ENOUGH_STAKE_SIZE_TO_BET_RETRY_${askIndex}`, data: { sol, iorsValues, iorsInfo } };
+    }
+  }
+
+  const winLimit = {
+    max: fixFloat(win_average * scaleForMax / (gold_side_a + gold_side_b + gold_side_c)),
+    min: fixFloat(win_average * scaleForMin / (gold_side_a + gold_side_b + gold_side_c)),
+  }
+
+  return { sol, iors: iorsValues, stakeLimit, winLimit };
+}
+
+export const getSoulutionBetResult = async ({ iors, iorsInfo, stakeLimit, stake=0 }) => {
+  const maxStake = stakeLimit.maxGroup.reduce((acc, curr) => acc + curr, 0);
+  const minStake = stakeLimit.minGroup.reduce((acc, curr) => acc + curr, 0);
+  let betStakeGroup = [];
+  if (stake > maxStake || stake < 0) {
+    stake = maxStake;
+    betStakeGroup = stakeLimit.maxGroup;
+  }
+  else if (stake < minStake) {
+    stake = minStake;
+    betStakeGroup = stakeLimit.minGroup;
+  }
+  else {
+    betStakeGroup = stakeLimit.proportions.map(p => p * stake);
+  }
+
+  const betInfo = iorsInfo.map((item, index) => {
+    if (item.asks) {
+      const bestPrice = +iors[index].bestPrice;
+      const stakeSize = fixFloat(betStakeGroup[index] / USDC_RATE / bestPrice, 0); // 必须保证买单金额小数不超过2位
+      return { ...item, stakeSize, bestPrice, betIndex: index, platform: 'polymarket' }
+    }
+    else if (item.info) {
+      const stakeSize = fixFloat(betStakeGroup[index], 0);
+      return { ...item, stakeSize, betIndex: index, platform: 'pinnacle' }
+    }
+  }).sort((a, b) => {
+    if (a.platform === 'polymarket' && b.platform === 'pinnacle') {
+      return -1;
+    }
+    else if (a.platform === 'pinnacle' && b.platform === 'polymarket') {
+      return 1;
+    }
+    else {
+      return 0;
+    }
+  });
+
+  // return { betInfo };
+  return runSequentially(betInfo.map(item => async() => {
+    if (item.asks) {
+      const result = await polymarketPlaceOrder(item);
+      return [result, item.betIndex]
+    }
+    else if (item.info) {
+      const result = await pinnaclePlaceOrder(item);
+      return [result, item.betIndex]
+    }
+  })).then(results => {
+    return results.sort((a, b) => a[1] - b[1]).map(item => item[0]);
+  }).catch(error => {
+    Logs.errDev(error);
+    return Promise.reject(error);
+  });
+}

+ 176 - 0
server/models/Platforms.js

@@ -0,0 +1,176 @@
+import { fork } from "child_process";
+
+import Store from "../state/store.js";
+import ProcessData from "../libs/processData.js";
+import Logs from "../libs/logs.js";
+
+// import { getPlatformIorInfo } from "./Markets.js";
+
+const getChildOptions = (inspect=9230) => {
+  return process.env.NODE_ENV == 'development' ? {
+    execArgv: [`--inspect=${inspect}`],
+    stdio: ['pipe', 'pipe', 'pipe', 'ipc']
+  } : {
+    stdio: ['pipe', 'pipe', 'pipe', 'ipc']
+  };
+}
+
+const triangleProcess = fork("triangle/main.js", [], getChildOptions(9229));
+const triangleData = new ProcessData(triangleProcess, 'triangle');
+
+triangleData.registerResponse('gamesRelations', async () => {
+  const gamesRelations = Object.values(Store.get('gamesRelations') ?? {});
+  const polymarketOdds = Store.get('polymarket', 'odds') ?? {};
+  const pinnacleOdds = Store.get('pinnacle', 'odds') ?? {};
+  const expireTime = Date.now() - 1000 * 15;
+  const { games: polymarketGames = [], timestamp: polymarketTimestamp = 0 } = polymarketOdds;
+  const { games: pinnacleGames = [], timestamp: pinnacleTimestamp = 0 } = pinnacleOdds;
+  const polymarketOddsMap = polymarketTimestamp > expireTime ? new Map(polymarketGames.map(item => [item.id, item])) : new Map();
+  const pinnacleOddsMap = pinnacleTimestamp > expireTime ? new Map(pinnacleGames.map(item => [item.id, item])) : new Map();
+  const newRelations = gamesRelations.map(relation => {
+    const { platforms: { polymarket, pinnacle }, ...rest } = relation;
+    const polymarketId = polymarket.id;
+    const pinnacleId = pinnacle.id;
+    const polymarketOdds = polymarketOddsMap.get(polymarketId)?.odds;
+    const pinnacleOdds = pinnacleOddsMap.get(pinnacleId)?.odds;
+    return { ...rest, platforms: {
+      polymarket: { ...polymarket, odds: polymarketOdds, evtime: polymarketTimestamp },
+      pinnacle: { ...pinnacle, odds: pinnacleOdds, evtime: pinnacleTimestamp },
+    }};
+  });
+  return Promise.resolve(newRelations);
+});
+
+triangleData.registerRequest('solutions', solutions => {
+  const oldSolutions = new Map((Store.get('solutions') ?? []).map(item => [item.sid, item]));
+  const newSolutions = new Map(solutions.map(item => [item.sid, item]));
+  const changed = {
+    add: [],
+    update: [],
+    remove: [],
+  }
+  oldSolutions.forEach((item, sid) => {
+    if (!newSolutions.has(sid)) {
+      changed.remove.push(sid);
+    }
+    else if (newSolutions.get(sid).sol.win_profit_rate != item.sol.win_profit_rate || JSON.stringify(newSolutions.get(sid).cpr) != JSON.stringify(item.cpr)) {
+      changed.update.push(sid);
+    }
+  });
+  newSolutions.forEach((item, sid) => {
+    if (!oldSolutions.has(sid)) {
+      changed.add.push(sid);
+    }
+  });
+  if (changed.update.length || changed.add.length || changed.remove.length) {
+    Store.set('solutions', solutions);
+  }
+});
+
+/**
+ * 通用的平台数据更新函数
+ * @param {string} platform - 平台名称
+ * @param {Array} newItems - 新的数据项数组
+ * @param {string} storeKey - Store 中的键名
+ * @returns {Promise}
+ */
+const updatePlatformData = async ({ platform, newItems, storeKey }) => {
+  if (!platform || !newItems?.length) {
+    return Promise.reject(new Error('invalid request', { cause: 400 }));
+  }
+
+  let changed = false;
+  const storeData = Store.get(storeKey) ?? {};
+  const { [platform]: storePlatformItems = [] } = storeData;
+  const storePlatformItemsMap = new Map(storePlatformItems.map(item => [item.id, item]));
+  const newPlatformItemsMap = new Map(newItems.map(item => [item.id, item]));
+
+  // 删除不存在的项
+  storePlatformItemsMap.forEach(item => {
+    if (!newPlatformItemsMap.has(item.id)) {
+      storePlatformItemsMap.delete(item.id);
+      changed = true;
+    }
+  });
+
+  // 添加新的项
+  newPlatformItemsMap.forEach(item => {
+    if (!storePlatformItemsMap.has(item.id)) {
+      storePlatformItemsMap.set(item.id, item);
+      changed = true;
+    }
+  });
+
+  // 更新 Store 中的数据
+  if (changed) {
+    const updatedPlatformItems = Array.from(storePlatformItemsMap.values());
+    storeData[platform] = updatedPlatformItems;
+    Store.set(storeKey, storeData);
+  }
+
+  return Promise.resolve();
+};
+
+/**
+ * 更新联赛数据
+ * @param {string} platform - 平台名称
+ * @param {Array} leagues - 联赛数据
+ * @returns
+ */
+export const updateLeagues = async ({ platform, leagues }) => {
+  return updatePlatformData({ platform, newItems: leagues, storeKey: 'leagues' });
+};
+
+/**
+ * 获取过滤后的联赛数据
+ * @param {string} platform - 平台名称
+ * @returns
+ */
+export const getFilteredLeagues = async (platform) => {
+  const polymarketLeagues = Store.get('polymarket', 'leagues') ?? [];
+  const polymarketLeaguesSet = new Set(polymarketLeagues.map(item => item.id));
+  const leaguesRelations = Store.get('leaguesRelations') ?? {};
+  const filteredLeagues = Object.values(leaguesRelations).filter(relation => {
+    return polymarketLeaguesSet.has(relation.platforms.polymarket.id);
+  }).map(relation => relation.platforms[platform]);
+  return filteredLeagues;
+}
+
+/**
+ * 更新比赛数据
+ * @param {string} platform - 平台名称
+ * @param {Array} games - 比赛数据
+ * @returns
+ */
+export const updateGames = async ({ platform, games }) => {
+  return updatePlatformData({ platform, newItems: games, storeKey: 'games' });
+};
+
+/**
+ * 获取过滤后的比赛数据
+ * @param {string} platform - 平台名称
+ * @returns
+ */
+export const getFilteredGames = async (platform) => {
+  const gamesRelations = Store.get('gamesRelations') ?? {};
+  const filteredGames = Object.values(gamesRelations).map(relation => relation.platforms[platform]);
+  return filteredGames;
+}
+
+/**
+ * 更新赔率数据
+ * @param {string} platform - 平台名称
+ * @param {Array} games - 赔率数据
+ * @param {number} timestamp - 时间戳
+ * @returns
+ */
+export const updateOdds = async ({ platform, games, timestamp }) => {
+  Store.set(platform, { games, timestamp }, 'odds');
+  return Promise.resolve();
+};
+
+export default {
+  updateLeagues, getFilteredLeagues,
+  updateGames, getFilteredGames,
+  updateOdds,
+};

+ 41 - 0
server/models/Translation.js

@@ -0,0 +1,41 @@
+import dotenv from 'dotenv';
+import { TranslationServiceClient } from '@google-cloud/translate';
+
+dotenv.config();
+
+const client = new TranslationServiceClient();
+
+const translateText = async (contents=[], targetLanguageCode='zh-CN') => {
+
+  if (typeof contents === 'string') {
+    contents = [contents];
+  }
+
+  else if (!Array.isArray(contents)) {
+    return Promise.reject(new Error('contents must be a string or an array of strings', { cause: 400 }));
+  }
+
+  if (contents.length === 0) {
+    return Promise.resolve([]);
+  };
+
+  const projectId = 'flyzto-cloud';
+  const location = 'global';
+
+  const request = {
+    parent: `projects/${projectId}/locations/${location}`,
+    contents,
+    targetLanguageCode,
+  };
+
+  return client.translateText(request)
+  .then(([response]) => {
+    const result = {};
+    response.translations.forEach((translation, index) => {
+      result[contents[index]] = translation.translatedText;
+    });
+    return result;
+  });
+}
+
+export default translateText;

+ 2336 - 0
server/package-lock.json

@@ -0,0 +1,2336 @@
+{
+  "name": "ppai",
+  "version": "0.0.1",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "ppai",
+      "version": "0.0.1",
+      "license": "ISC",
+      "dependencies": {
+        "@google-cloud/translate": "^9.3.0",
+        "cookie-parser": "^1.4.7",
+        "dayjs": "^1.11.19",
+        "dotenv": "^17.2.3",
+        "express": "^5.2.1",
+        "express-ws": "^5.0.2"
+      }
+    },
+    "node_modules/@google-cloud/common": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/@google-cloud/common/-/common-6.0.0.tgz",
+      "integrity": "sha512-IXh04DlkLMxWgYLIUYuHHKXKOUwPDzDgke1ykkkJPe48cGIS9kkL2U/o0pm4ankHLlvzLF/ma1eO86n/bkumIA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@google-cloud/projectify": "^4.0.0",
+        "@google-cloud/promisify": "^4.0.0",
+        "arrify": "^2.0.0",
+        "duplexify": "^4.1.3",
+        "extend": "^3.0.2",
+        "google-auth-library": "^10.0.0-rc.1",
+        "html-entities": "^2.5.2",
+        "retry-request": "^8.0.0",
+        "teeny-request": "^10.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@google-cloud/common/node_modules/@google-cloud/promisify": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/@google-cloud/promisify/-/promisify-4.1.0.tgz",
+      "integrity": "sha512-G/FQx5cE/+DqBbOpA5jKsegGwdPniU6PuIEMt+qxWgFxvxuFOzVmp6zYchtYuwAWV5/8Dgs0yAmjvNZv3uXLQg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@google-cloud/projectify": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/@google-cloud/projectify/-/projectify-4.0.0.tgz",
+      "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@google-cloud/promisify": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/@google-cloud/promisify/-/promisify-5.0.0.tgz",
+      "integrity": "sha512-N8qS6dlORGHwk7WjGXKOSsLjIjNINCPicsOX6gyyLiYk7mq3MtII96NZ9N2ahwA2vnkLmZODOIH9rlNniYWvCQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@google-cloud/translate": {
+      "version": "9.3.0",
+      "resolved": "https://registry.npmmirror.com/@google-cloud/translate/-/translate-9.3.0.tgz",
+      "integrity": "sha512-OgZ2bCu3P0ZzMhEdYubwyCo/eFFlJMYalozmgOxlVcD51vCYelYUJeVnGlS+3cFQTJQX4RE84bYTKu7W0wqByw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@google-cloud/common": "^6.0.0",
+        "@google-cloud/promisify": "^5.0.0",
+        "arrify": "^2.0.0",
+        "extend": "^3.0.2",
+        "google-gax": "^5.0.0",
+        "is-html": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@grpc/grpc-js": {
+      "version": "1.14.3",
+      "resolved": "https://registry.npmmirror.com/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
+      "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@grpc/proto-loader": "^0.8.0",
+        "@js-sdsl/ordered-map": "^4.4.2"
+      },
+      "engines": {
+        "node": ">=12.10.0"
+      }
+    },
+    "node_modules/@grpc/proto-loader": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
+      "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "lodash.camelcase": "^4.3.0",
+        "long": "^5.0.0",
+        "protobufjs": "^7.5.3",
+        "yargs": "^17.7.2"
+      },
+      "bin": {
+        "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@js-sdsl/ordered-map": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmmirror.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
+      "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/js-sdsl"
+      }
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@protobufjs/aspromise": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/base64": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz",
+      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/codegen": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/eventemitter": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/fetch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+      "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.1",
+        "@protobufjs/inquire": "^1.1.0"
+      }
+    },
+    "node_modules/@protobufjs/float": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz",
+      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/inquire": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/path": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz",
+      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/pool": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz",
+      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/utf8": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@tootallnate/once": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/@tootallnate/once/-/once-2.0.0.tgz",
+      "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "25.0.10",
+      "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.0.10.tgz",
+      "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.16.0"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz",
+      "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "^3.0.0",
+        "negotiator": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/arrify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/arrify/-/arrify-2.0.1.tgz",
+      "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "license": "MIT"
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/bignumber.js": {
+      "version": "9.3.1",
+      "resolved": "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.1.tgz",
+      "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/body-parser": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.2.tgz",
+      "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "^3.1.2",
+        "content-type": "^1.0.5",
+        "debug": "^4.4.3",
+        "http-errors": "^2.0.0",
+        "iconv-lite": "^0.7.0",
+        "on-finished": "^2.4.1",
+        "qs": "^6.14.1",
+        "raw-body": "^3.0.1",
+        "type-is": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/cliui/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/cliui/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/cliui/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
+    "node_modules/content-disposition": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.0.1.tgz",
+      "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-parser": {
+      "version": "1.4.7",
+      "resolved": "https://registry.npmmirror.com/cookie-parser/-/cookie-parser-1.4.7.tgz",
+      "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
+      "license": "MIT",
+      "dependencies": {
+        "cookie": "0.7.2",
+        "cookie-signature": "1.0.6"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+      "license": "MIT"
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/data-uri-to-buffer": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+      "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.19",
+      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz",
+      "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "17.2.3",
+      "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.2.3.tgz",
+      "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/duplexify": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmmirror.com/duplexify/-/duplexify-4.1.3.tgz",
+      "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
+      "license": "MIT",
+      "dependencies": {
+        "end-of-stream": "^1.4.1",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.1.1",
+        "stream-shift": "^1.0.2"
+      }
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "license": "MIT"
+    },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/end-of-stream": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz",
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+      "license": "MIT",
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmmirror.com/express/-/express-5.2.1.tgz",
+      "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "^2.0.0",
+        "body-parser": "^2.2.1",
+        "content-disposition": "^1.0.0",
+        "content-type": "^1.0.5",
+        "cookie": "^0.7.1",
+        "cookie-signature": "^1.2.1",
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "finalhandler": "^2.1.0",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "merge-descriptors": "^2.0.0",
+        "mime-types": "^3.0.0",
+        "on-finished": "^2.4.1",
+        "once": "^1.4.0",
+        "parseurl": "^1.3.3",
+        "proxy-addr": "^2.0.7",
+        "qs": "^6.14.0",
+        "range-parser": "^1.2.1",
+        "router": "^2.2.0",
+        "send": "^1.1.0",
+        "serve-static": "^2.2.0",
+        "statuses": "^2.0.1",
+        "type-is": "^2.0.1",
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/express-ws": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmmirror.com/express-ws/-/express-ws-5.0.2.tgz",
+      "integrity": "sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "ws": "^7.4.6"
+      },
+      "engines": {
+        "node": ">=4.5.0"
+      },
+      "peerDependencies": {
+        "express": "^4.0.0 || ^5.0.0-alpha.1"
+      }
+    },
+    "node_modules/express/node_modules/cookie-signature": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz",
+      "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.6.0"
+      }
+    },
+    "node_modules/extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "license": "MIT"
+    },
+    "node_modules/fetch-blob": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/fetch-blob/-/fetch-blob-3.2.0.tgz",
+      "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "paypal",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "node-domexception": "^1.0.0",
+        "web-streams-polyfill": "^3.0.3"
+      },
+      "engines": {
+        "node": "^12.20 || >= 14.13"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz",
+      "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "on-finished": "^2.4.1",
+        "parseurl": "^1.3.3",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/formdata-polyfill": {
+      "version": "4.0.10",
+      "resolved": "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+      "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+      "license": "MIT",
+      "dependencies": {
+        "fetch-blob": "^3.1.2"
+      },
+      "engines": {
+        "node": ">=12.20.0"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gaxios": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmmirror.com/gaxios/-/gaxios-7.1.3.tgz",
+      "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "extend": "^3.0.2",
+        "https-proxy-agent": "^7.0.1",
+        "node-fetch": "^3.3.2",
+        "rimraf": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/gcp-metadata": {
+      "version": "8.1.2",
+      "resolved": "https://registry.npmmirror.com/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
+      "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "gaxios": "^7.0.0",
+        "google-logging-utils": "^1.0.0",
+        "json-bigint": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/glob": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmmirror.com/glob/-/glob-10.5.0.tgz",
+      "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/google-auth-library": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmmirror.com/google-auth-library/-/google-auth-library-10.5.0.tgz",
+      "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "base64-js": "^1.3.0",
+        "ecdsa-sig-formatter": "^1.0.11",
+        "gaxios": "^7.0.0",
+        "gcp-metadata": "^8.0.0",
+        "google-logging-utils": "^1.0.0",
+        "gtoken": "^8.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/google-gax": {
+      "version": "5.0.6",
+      "resolved": "https://registry.npmmirror.com/google-gax/-/google-gax-5.0.6.tgz",
+      "integrity": "sha512-1kGbqVQBZPAAu4+/R1XxPQKP0ydbNYoLAr4l0ZO2bMV0kLyLW4I1gAk++qBLWt7DPORTzmWRMsCZe86gDjShJA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@grpc/grpc-js": "^1.12.6",
+        "@grpc/proto-loader": "^0.8.0",
+        "duplexify": "^4.1.3",
+        "google-auth-library": "^10.1.0",
+        "google-logging-utils": "^1.1.1",
+        "node-fetch": "^3.3.2",
+        "object-hash": "^3.0.0",
+        "proto3-json-serializer": "^3.0.0",
+        "protobufjs": "^7.5.3",
+        "retry-request": "^8.0.0",
+        "rimraf": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/google-logging-utils": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmmirror.com/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
+      "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gtoken": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/gtoken/-/gtoken-8.0.0.tgz",
+      "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
+      "license": "MIT",
+      "dependencies": {
+        "gaxios": "^7.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/html-entities": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmmirror.com/html-entities/-/html-entities-2.6.0.tgz",
+      "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/mdevils"
+        },
+        {
+          "type": "patreon",
+          "url": "https://patreon.com/mdevils"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/html-tags": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmmirror.com/html-tags/-/html-tags-3.3.1.tgz",
+      "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz",
+      "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "inherits": "~2.0.4",
+        "setprototypeof": "~1.2.0",
+        "statuses": "~2.0.2",
+        "toidentifier": "~1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/http-proxy-agent": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+      "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+      "license": "MIT",
+      "dependencies": {
+        "@tootallnate/once": "2",
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/http-proxy-agent/node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz",
+      "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-html": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/is-html/-/is-html-2.0.0.tgz",
+      "integrity": "sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg==",
+      "license": "MIT",
+      "dependencies": {
+        "html-tags": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-promise": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz",
+      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+      "license": "MIT"
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "license": "ISC"
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/json-bigint": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz",
+      "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "bignumber.js": "^9.0.0"
+      }
+    },
+    "node_modules/jwa": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz",
+      "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "^1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jws": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz",
+      "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+      "license": "MIT"
+    },
+    "node_modules/long": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
+      "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "license": "ISC"
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz",
+      "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+      "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz",
+      "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/negotiator": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz",
+      "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/node-domexception": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz",
+      "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+      "deprecated": "Use your platform's native DOMException instead",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "github",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.5.0"
+      }
+    },
+    "node_modules/node-fetch": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz",
+      "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+      "license": "MIT",
+      "dependencies": {
+        "data-uri-to-buffer": "^4.0.0",
+        "fetch-blob": "^3.1.4",
+        "formdata-polyfill": "^4.0.10"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/node-fetch"
+      }
+    },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+      "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/proto3-json-serializer": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz",
+      "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "protobufjs": "^7.4.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/protobufjs": {
+      "version": "7.5.4",
+      "resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.5.4.tgz",
+      "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
+      "hasInstallScript": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.2",
+        "@protobufjs/base64": "^1.1.2",
+        "@protobufjs/codegen": "^2.0.4",
+        "@protobufjs/eventemitter": "^1.1.0",
+        "@protobufjs/fetch": "^1.1.0",
+        "@protobufjs/float": "^1.0.2",
+        "@protobufjs/inquire": "^1.1.0",
+        "@protobufjs/path": "^1.1.2",
+        "@protobufjs/pool": "^1.1.0",
+        "@protobufjs/utf8": "^1.1.0",
+        "@types/node": ">=13.7.0",
+        "long": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.14.1",
+      "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.1.tgz",
+      "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz",
+      "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.7.0",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/retry-request": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmmirror.com/retry-request/-/retry-request-8.0.2.tgz",
+      "integrity": "sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw==",
+      "license": "MIT",
+      "dependencies": {
+        "extend": "^3.0.2",
+        "teeny-request": "^10.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "5.0.10",
+      "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-5.0.10.tgz",
+      "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^10.3.7"
+      },
+      "bin": {
+        "rimraf": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/router": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/router/-/router-2.2.0.tgz",
+      "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "is-promise": "^4.0.0",
+        "parseurl": "^1.3.3",
+        "path-to-regexp": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/send": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/send/-/send-1.2.1.tgz",
+      "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.3",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.1",
+        "mime-types": "^3.0.2",
+        "ms": "^2.1.3",
+        "on-finished": "^2.4.1",
+        "range-parser": "^1.2.1",
+        "statuses": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/serve-static": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.1.tgz",
+      "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "parseurl": "^1.3.3",
+        "send": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/stream-events": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/stream-events/-/stream-events-1.0.5.tgz",
+      "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
+      "license": "MIT",
+      "dependencies": {
+        "stubs": "^3.0.0"
+      }
+    },
+    "node_modules/stream-shift": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/stream-shift/-/stream-shift-1.0.3.tgz",
+      "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
+      "license": "MIT"
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.2.tgz",
+      "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/stubs": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/stubs/-/stubs-3.0.0.tgz",
+      "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
+      "license": "MIT"
+    },
+    "node_modules/teeny-request": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmmirror.com/teeny-request/-/teeny-request-10.1.0.tgz",
+      "integrity": "sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "http-proxy-agent": "^5.0.0",
+        "https-proxy-agent": "^5.0.0",
+        "node-fetch": "^3.3.2",
+        "stream-events": "^1.0.5"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/teeny-request/node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/teeny-request/node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.0.1.tgz",
+      "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+      "license": "MIT",
+      "dependencies": {
+        "content-type": "^1.0.5",
+        "media-typer": "^1.1.0",
+        "mime-types": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "7.16.0",
+      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
+      "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+      "license": "MIT"
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "license": "MIT"
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/web-streams-polyfill": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+      "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    },
+    "node_modules/ws": {
+      "version": "7.5.10",
+      "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.10.tgz",
+      "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.3.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": "^5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs": {
+      "version": "17.7.2",
+      "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/yargs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    }
+  }
+}

+ 23 - 0
server/package.json

@@ -0,0 +1,23 @@
+{
+  "name": "ppai",
+  "version": "0.0.1",
+  "description": "polymarket vs pinnacle",
+  "main": "main.js",
+  "type": "module",
+  "scripts": {
+    "dev": "nodemon --ignore data/ --ignore cache/ --ignore node_modules/ --inspect=9226 main.js",
+    "dev:polymarket": "nodemon --ignore data/ --ignore cache/ --ignore node_modules/ --inspect=9227 polymarket/main.js",
+    "dev:pinnacle": "nodemon --ignore data/ --ignore cache/ --ignore node_modules/ --inspect=9228 pinnacle/main.js",
+    "dev:triangle": "nodemon --ignore data/ --ignore cache/ --ignore node_modules/ --inspect=9229 triangle/main.js"
+  },
+  "author": "qboss",
+  "license": "ISC",
+  "dependencies": {
+    "@google-cloud/translate": "^9.3.0",
+    "cookie-parser": "^1.4.7",
+    "dayjs": "^1.11.19",
+    "dotenv": "^17.2.3",
+    "express": "^5.2.1",
+    "express-ws": "^5.0.2"
+  }
+}

+ 120 - 0
server/routes/games.js

@@ -0,0 +1,120 @@
+import express from 'express';
+const router = express.Router();
+
+import Games from '../models/Games.js';
+
+router.get('/get_leagues', (req, res) => {
+  Games.getLeagues()
+  .then(leagues => {
+    res.sendSuccess(leagues);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.post('/set_leagues_relation', (req, res) => {
+  Games.setLeaguesRelation(req.body)
+  .then(() => {
+    res.sendSuccess();
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.post('/remove_leagues_relation', (req, res) => {
+  const { id } = req.body;
+  Games.removeLeaguesRelation(id)
+  .then(() => {
+    res.sendSuccess();
+  }).catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.get('/get_leagues_relations', (req, res) => {
+  Games.getLeaguesRelations()
+  .then(relations => {
+    res.sendSuccess(relations);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.get('/get_games', (req, res) => {
+  Games.getGames()
+  .then(games => {
+    res.sendSuccess(games);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.post('/set_relation', (req, res) => {
+  Games.setGamesRelation(req.body)
+  .then(() => {
+    res.sendSuccess();
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.post('/remove_relation', (req, res) => {
+  const { id } = req.body;
+  Games.removeGamesRelation(id)
+  .then(() => {
+    res.sendSuccess();
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.get('/get_relations', (req, res) => {
+  Games.getGamesRelations()
+  .then(relations => {
+    res.sendSuccess(relations);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.get('/get_solutions', (req, res) => {
+  const { min_profit_rate } = req.query;
+  Games.getSolutions({ min_profit_rate })
+  .then(solutions => {
+    res.sendSuccess(solutions);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.get('/get_solution_ior_info', (req, res) => {
+  const { sid } = req.query;
+  Games.getSolutionIorsInfo(sid)
+  .then(iorInfo => {
+    res.sendSuccess(iorInfo);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.get('/bet_solution', (req, res) => {
+  const { sid, stake } = req.query;
+  Games.betSolution(sid, stake)
+  .then(betInfo => {
+    res.sendSuccess(betInfo);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+export default router;

+ 38 - 0
server/routes/locales.js

@@ -0,0 +1,38 @@
+import express from 'express';
+const router = express.Router();
+
+import Locales from '../models/Locales.js';
+
+router.get('/get_locales', (req, res) => {
+  Locales.getLocales()
+  .then(locales => {
+    res.sendSuccess(locales);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.post('/set_locales', (req, res) => {
+  const locales = req.body;
+  Locales.setLocales(locales)
+  .then(() => {
+    res.sendSuccess();
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.post('/remove_locales', (req, res) => {
+  const keys = req.body;
+  Locales.removeLocales(keys)
+  .then(() => {
+    res.sendSuccess();
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+export default router;

+ 60 - 0
server/routes/platforms.js

@@ -0,0 +1,60 @@
+import express from 'express';
+const router = express.Router();
+
+import Platforms from '../models/Platforms.js';
+
+router.post('/update_leagues', (req, res) => {
+  const { platform, leagues } = req.body;
+  Platforms.updateLeagues({ platform, leagues })
+  .then(() => {
+    res.sendSuccess();
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.get('/get_filtered_leagues', (req, res) => {
+  const { platform } = req.query;
+  Platforms.getFilteredLeagues(platform).then(leagues => {
+    res.sendSuccess(leagues);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.post('/update_games', (req, res) => {
+  const { platform, games } = req.body;
+  Platforms.updateGames({ platform, games })
+  .then(() => {
+    res.sendSuccess();
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.get('/get_filtered_games', (req, res) => {
+  const { platform } = req.query;
+  Platforms.getFilteredGames(platform)
+  .then(games => {
+    res.sendSuccess(games);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.post('/update_odds', (req, res) => {
+  const { platform, games, timestamp } = req.body;
+  Platforms.updateOdds({ platform, games, timestamp })
+  .then(() => {
+    res.sendSuccess();
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+export default router;

+ 112 - 0
server/state/locales.js

@@ -0,0 +1,112 @@
+import path from "path";
+import { fileURLToPath } from "url";
+
+import Logs from "../libs/logs.js";
+import Cache from "../libs/cache.js";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const LOCALES_FILE = path.join(__dirname, '../data/locales.json');
+
+const LOCALES_DATA = {};
+
+const isPlainObject = (obj) => {
+  if (Object.prototype.toString.call(obj) !== '[object Object]') return false;
+
+  const proto = Object.getPrototypeOf(obj);
+  return proto === Object.prototype || proto === null;
+}
+
+const initLocales = () => {
+  Cache.getData(LOCALES_FILE).then(data => {
+    Logs.out('load locales data', data);
+    Object.assign(LOCALES_DATA, data);
+  }).catch(error => {
+    Logs.out('not load locales data', error.message);
+  });
+}
+
+const saveLocales = () => {
+  Cache.setData(LOCALES_FILE, LOCALES_DATA).catch(error => {
+    Logs.out('save cache failed', error);
+  });
+}
+
+export const get = async (keys) => {
+  return new Promise((resolve, reject) => {
+    if (typeof(keys) === 'undefined') {
+      resolve(LOCALES_DATA);
+    }
+    else if (typeof(keys) === 'string') {
+      resolve(LOCALES_DATA[keys] ?? null);
+    }
+    else if (Array.isArray(keys) && keys.every(key => typeof(key) === 'string')) {
+      const result = {};
+      keys.forEach(key => {
+        result[key] = LOCALES_DATA[key] ?? null;
+      });
+      resolve(result);
+    }
+    else {
+      reject(new Error('invalid keys'));
+    }
+  });
+}
+
+export const set = async (key, value) => {
+  return new Promise((resolve, reject) => {
+    if (typeof(key) === 'string' && typeof(value) !== 'undefined') {
+      LOCALES_DATA[key] = value;
+      saveLocales();
+      resolve();
+    }
+    else if (isPlainObject(key)
+    && Object.keys(key).every(k => typeof(k) === 'string')
+    && Object.values(key).every(v => typeof(v) === 'string')) {
+      Object.assign(LOCALES_DATA, key);
+      saveLocales();
+      resolve();
+    }
+    else {
+      reject(new Error('invalid key or value'));
+    }
+  });
+}
+
+export const remove = async (keys) => {
+  return new Promise((resolve, reject) => {
+    if (typeof(keys) === 'string') {
+      delete LOCALES_DATA[keys];
+      saveLocales();
+      resolve();
+    }
+    else if (Array.isArray(keys) && keys.every(key => typeof(key) === 'string')) {
+      keys.forEach(key => {
+        delete LOCALES_DATA[key];
+      });
+      saveLocales();
+      resolve();
+    }
+    else {
+      reject(new Error('invalid keys'));
+    }
+  });
+}
+
+// 进程结束时保存数据
+process.on('exit', saveLocales);
+process.on('SIGINT', () => {
+  process.exit(0);
+});
+process.on('SIGTERM', () => {
+  process.exit(0);
+});
+process.on('SIGUSR2', () => {
+  process.exit(0);
+});
+
+// 初始化缓存
+initLocales();
+
+export default { get, set, remove };

+ 147 - 0
server/state/store.js

@@ -0,0 +1,147 @@
+import path from "path";
+import { fileURLToPath } from "url";
+
+import Logs from "../libs/logs.js";
+import Cache from "../libs/cache.js";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const STORE_FILE = path.join(__dirname, '../data/store.json');
+
+const STORE_DATA = {};
+const STORE_OPTIONS = {
+  saveVersion: 0,
+  updateVersion: 0,
+};
+
+/**
+ * 判断是否为普通对象
+ * @param {*} obj
+ * @returns
+ */
+const isPlainObject = (obj) => {
+  if (Object.prototype.toString.call(obj) !== '[object Object]') return false;
+
+  const proto = Object.getPrototypeOf(obj);
+  return proto === Object.prototype || proto === null;
+}
+
+/**
+ * 初始化数据
+ */
+const initStore = () => {
+  Cache.getData(STORE_FILE).then(data => {
+    Logs.out('load store data', data);
+    Object.assign(STORE_DATA, data);
+  }).catch(error => {
+    Logs.out('not load store data', error.message);
+  });
+}
+
+/**
+ * 保存数据
+ */
+const saveStore = () => {
+  Cache.setData(STORE_FILE, STORE_DATA, 0).catch(error => {
+    Logs.out('save cache failed', error);
+  });
+}
+
+/**
+ * 获取数据
+ * @param {*} key
+ * @param {*} property
+ */
+export const get = (key, property) => {
+  if (!key) {
+    return STORE_DATA;
+  }
+  if (property) {
+    return STORE_DATA[property]?.[key] ?? null;
+  }
+  return STORE_DATA[key] ?? null;
+}
+
+/**
+ * 设置数据
+ * @param {*} key
+ * @param {*} value
+ * @param {*} property
+ */
+export const set = (key, value, property) => {
+  if (typeof(key) == 'string' && typeof(value) != 'undefined') {
+    if (property && typeof(property) == 'string') {
+      if (!STORE_DATA[property]) {
+        STORE_DATA[property] = {};
+      }
+      STORE_DATA[property][key] = value;
+    }
+    else {
+      STORE_DATA[key] = value;
+    }
+    STORE_OPTIONS.updateVersion = Date.now();
+  }
+  else if (isPlainObject(key)) {
+    if (value && typeof(value) == 'string') {
+      property = value;
+      value = key;
+      if (!STORE_DATA[property]) {
+        STORE_DATA[property] = {};
+      }
+      Object.assign(STORE_DATA[property], value);
+    }
+    else {
+      value = key;
+      Object.assign(STORE_DATA, value);
+    }
+    STORE_OPTIONS.updateVersion = Date.now();
+  }
+  else {
+    Logs.out('invalid key or value', key, value);
+  }
+}
+
+export const remove = (key, property) => {
+  if (property && STORE_DATA[property]) {
+    delete STORE_DATA[property][key];
+  }
+  else if (!property) {
+    delete STORE_DATA[key];
+  }
+  STORE_OPTIONS.updateVersion = Date.now();
+}
+
+/**
+ * 自动保存数据
+ */
+const autoSaveStore = () => {
+  if (STORE_OPTIONS.updateVersion > STORE_OPTIONS.saveVersion) {
+    saveStore();
+    STORE_OPTIONS.saveVersion = STORE_OPTIONS.updateVersion;
+  }
+  setTimeout(() => {
+    autoSaveStore();
+  }, 1000 * 60);
+}
+
+/**
+ * 进程结束时保存数据
+ */
+process.on('exit', saveStore);
+process.on('SIGINT', () => {
+  process.exit(0);
+});
+process.on('SIGTERM', () => {
+  process.exit(0);
+});
+process.on('SIGUSR2', () => {
+  process.exit(0);
+});
+
+// 初始化缓存
+initStore();
+// 自动保存数据
+autoSaveStore();
+
+export default { get, set, remove };

+ 24 - 0
server/state/wsclients.js

@@ -0,0 +1,24 @@
+import Logs from "../libs/logs.js";
+
+const WS_CLIENTS = {};
+
+const setWebSocketClient = (platform, ws) => {
+  if (!platform || !ws) {
+    return;
+  }
+  WS_CLIENTS[platform] = ws;
+}
+
+const getWebSocketClient = (platform) => {
+  return WS_CLIENTS[platform] ?? null;
+}
+
+const removeWebSocketClient = (platform) => {
+  delete WS_CLIENTS[platform];
+}
+
+export default {
+  setWebSocketClient,
+  getWebSocketClient,
+  removeWebSocketClient,
+}

+ 209 - 0
server/triangle/eventSolutions.js

@@ -0,0 +1,209 @@
+/**
+ * 精确浮点数字
+ * @param {number} number
+ * @param {number} x
+ * @returns {number}
+ */
+const fixFloat = (number, x=2) => {
+  return parseFloat(number.toFixed(x));
+}
+
+/**
+ * 计算盈利
+ */
+const triangleProfitCalc = (betInfo) => {
+  const {
+    cross_type,
+    gold_side_a: x,
+    gold_side_b: y,
+    gold_side_c: z,
+    odds_side_a: a,
+    odds_side_b: b,
+    odds_side_c: c,
+    rebate_side_a: A = 0,
+    rebate_side_b: B = 0,
+    rebate_side_c: C = 0,
+    rebate_type_side_a: TA = 0,
+    rebate_type_side_b: TB = 0,
+    rebate_type_side_c: TC = 0,
+  } = betInfo;
+
+  /**
+   * cross_type:
+   *  la: 全输
+   *  wa: 全赢
+   *  lh: 半输
+   *  wh: 半赢
+   *  dr: 和局
+   *  la_wh_wa, la_dr_wa, la_lh_wa, lh_dr_wa, lh_lh_wa, la_la_wa
+   */
+
+  const k1 = TA == 1 ? a + A : a * (1 + A);
+  const k2 = 1 - A;
+  const k3 = TB == 1 ? b + B : b * (1 + B);
+  const k4 = 1 - B;
+  const k5 = TC == 1 ? c + C : c * (1 + C);
+  const k6 = 1 - C;
+
+  let win_side_a = 0, win_side_b = 0, win_side_c = 0;
+  win_side_a = k1*x - k4*y - k6*z;
+  win_side_b = k3*y - k2*x - k6*z;
+
+  switch (cross_type) {
+    case 'la_wh_wa': // 全输 半赢 全赢
+      win_side_c = k5*z - k2*x + k3*y/2;
+      break;
+    case 'la_dr_wa': // 全输 和局 全赢
+      win_side_c = k5*z - k2*x;
+      break;
+    case 'la_lh_wa': // 全输 半输 全赢
+      win_side_c = k5*z - k2*x - k4*y/2;
+      break;
+    case 'lh_dr_wa': // 半输 和局 全赢
+      win_side_c = k5*z - k2*x/2;
+      break;
+    case 'lh_lh_wa': // 半输 半输 全赢
+      win_side_c = k5*z - k2*x/2 - k4*y/2;
+      break;
+    case 'la_la_wa': // 全输 全输 全赢
+      win_side_c = k5*z - k2*x - k4*y;
+      break;
+    case 'la_wa_rv': // 两盘口输赢
+      win_side_c = 0;
+      break;
+    default:
+      win_side_c = 0;
+  }
+
+  win_side_a = fixFloat(win_side_a);
+  win_side_b = fixFloat(win_side_b);
+  win_side_c = fixFloat(win_side_c);
+  let win_average = 0;
+  if (cross_type == 'la_wa_rv') {
+    win_side_c = 0;
+    win_average = fixFloat((win_side_a + win_side_b) / 2);
+  }
+  else {
+    win_average = fixFloat((win_side_a + win_side_b + win_side_c) / 3);
+  }
+
+  return { win_side_a, win_side_b, win_side_c, win_average }
+}
+
+const triangleGoldCalc = (betInfo) => {
+  const {
+    cross_type,
+    base_index = 0,
+    base_stake = 10000,
+    odds_side_a: a,
+    odds_side_b: b,
+    odds_side_c: c,
+    rebate_side_a: A = 0,
+    rebate_side_b: B = 0,
+    rebate_side_c: C = 0,
+    rebate_type_side_a: TA = 0,
+    rebate_type_side_b: TB = 0,
+    rebate_type_side_c: TC = 0,
+  } = betInfo;
+  if (typeof a !== 'number' || typeof b !== 'number' || typeof c !== 'number') {
+    return;
+  }
+  const k1 = TA == 1 ? a + A : a * (1 + A);
+  const k2 = 1 - A;
+  const k3 = TB == 1 ? b + B : b * (1 + B);
+  const k4 = 1 - B;
+  const k5 = TC == 1 ? c + C : c * (1 + C);
+  const k6 = 1 - C;
+  let x = base_stake;
+  let y = (k1 + k2) * x / (k3 + k4);
+  let z;
+  switch (cross_type) {
+    case 'la_wh_wa': // 全输 半赢 全赢
+      z = k3 * y / 2 / (k5 + k6);
+      break;
+    case 'la_dr_wa': // 全输 和局 全赢
+      z = k3 * y / (k5 + k6);
+      break;
+    case 'la_lh_wa': // 全输 半输 全赢
+      z = (k3 + k4 / 2) * y / (k5 + k6);
+      break;
+    case 'lh_dr_wa': // 半输 和局 全赢
+      z = (k3 * y - k2 * x / 2) / (k5 + k6);
+      break;
+    case 'lh_lh_wa': // 半输 半输 全赢
+      z = ((k3 + k4/2) * y - k2 * x / 2) / (k5 + k6);
+      break;
+    case 'la_la_wa': // 全输 全输 全赢
+      z = (k3 + k4) * y / (k5 + k6);
+      break;
+    case 'la_wa_rv': // 两盘口输赢
+      z = 0;
+      break;
+    default:
+      z = 0;
+  }
+
+  if (base_index == 1) {
+    const scale = base_stake / y;
+    x = x * scale;
+    y = base_stake;
+    z = z * scale;
+  }
+  else if (base_index == 2) {
+    const scale = base_stake / z;
+    x = x * scale;
+    y = y * scale;
+    z = base_stake;
+  }
+
+  if (x < 0 || y < 0 || z < 0) {
+    return;
+  }
+
+  return {
+    gold_side_a: fixFloat(x),
+    gold_side_b: fixFloat(y),
+    gold_side_c: fixFloat(z),
+  };
+
+}
+
+const eventSolutions = (betInfo, showGolds=false) => {
+  const goldsInfo = triangleGoldCalc(betInfo);
+  if (!goldsInfo) {
+    return;
+  }
+  const profitInfo = triangleProfitCalc({ ...betInfo, ...goldsInfo }, showGolds);
+
+  const { gold_side_a, gold_side_b, gold_side_c } = goldsInfo;
+  const { win_side_a, win_side_b, win_side_c, win_average } = profitInfo;
+
+  const {
+    cross_type, base_index = 0, base_stake = 10000,
+    odds_side_a, odds_side_b, odds_side_c,
+    rebate_side_a, rebate_side_b, rebate_side_c,
+    rebate_type_side_a, rebate_type_side_b, rebate_type_side_c,
+  } = betInfo;
+
+  // const win_average_rate = fixFloat(win_average / base_stake * 100);
+  const win_profit_rate = fixFloat(win_average / (gold_side_a + gold_side_b + gold_side_c) * 100);
+
+  let result = {
+    odds_side_a, odds_side_b, odds_side_c,
+    rebate_side_a, rebate_side_b, rebate_side_c,
+    rebate_type_side_a, rebate_type_side_b, rebate_type_side_c,
+    win_average, /*win_average_rate,*/ win_profit_rate, cross_type,
+    base_index, base_stake,
+  }
+
+  if (showGolds) {
+    result = {
+      ...result,
+      gold_side_a, gold_side_b, gold_side_c,
+      win_side_a, win_side_b, win_side_c,
+    }
+  }
+  return result;
+}
+
+export default eventSolutions;

+ 370 - 0
server/triangle/iorKeys.js

@@ -0,0 +1,370 @@
+const iorKeys = {
+  'A:0': [
+    ['ior_mh', 'ior_rac_025', 'ior_mn', 'la_wh_wa'],
+    ['ior_mc', 'ior_rah_025', 'ior_mn', 'la_wh_wa'],
+    ['ior_rh_05', 'ior_rac_025', 'ior_mn', 'la_wh_wa'],
+    ['ior_rc_05', 'ior_rah_025', 'ior_mn', 'la_wh_wa'],
+
+    ['ior_mh', 'ior_rc_0', 'ior_mn', 'la_dr_wa'],
+    ['ior_mc', 'ior_rh_0', 'ior_mn', 'la_dr_wa'],
+    ['ior_rh_05', 'ior_rc_0', 'ior_mn', 'la_dr_wa'],
+    ['ior_rc_05', 'ior_rh_0', 'ior_mn', 'la_dr_wa'],
+
+    ['ior_mh', 'ior_rc_025', 'ior_mn', 'la_lh_wa'],
+    ['ior_mc', 'ior_rh_025', 'ior_mn', 'la_lh_wa'],
+    ['ior_rh_05', 'ior_rc_025', 'ior_mn', 'la_lh_wa'],
+    ['ior_rc_05', 'ior_rh_025', 'ior_mn', 'la_lh_wa'],
+
+    ['ior_mh', 'ior_mc', 'ior_mn', 'la_la_wa'],
+    ['ior_rh_05', 'ior_rc_05', 'ior_mn', 'la_la_wa'],
+    ['ior_mh', 'ior_rc_05', 'ior_mn', 'la_la_wa'],
+    ['ior_rh_05', 'ior_mc', 'ior_mn', 'la_la_wa'],
+
+    ['ior_rh_025', 'ior_rc_0', 'ior_mn', 'lh_dr_wa'],
+    ['ior_rc_025', 'ior_rh_0', 'ior_mn', 'lh_dr_wa'],
+
+    ['ior_rh_025', 'ior_rc_025', 'ior_mn', 'lh_lh_wa'],
+  ],
+  'A:1': [
+    ['ior_rh_15', 'ior_rac_125', 'ior_wmh_1', 'la_wh_wa'],
+    ['ior_rc_15', 'ior_rah_125', 'ior_wmc_1', 'la_wh_wa'],
+
+    ['ior_rh_15', 'ior_rac_1', 'ior_wmh_1', 'la_dr_wa'],
+    ['ior_rc_15', 'ior_rah_1', 'ior_wmc_1', 'la_dr_wa'],
+
+    ['ior_rh_15', 'ior_rac_075', 'ior_wmh_1', 'la_lh_wa'],
+    ['ior_rc_15', 'ior_rah_075', 'ior_wmc_1', 'la_lh_wa'],
+
+    ['ior_rh_15', 'ior_rac_05', 'ior_wmh_1', 'la_la_wa'],
+    ['ior_rh_15', 'ior_moh', 'ior_wmh_1', 'la_la_wa'],
+    ['ior_rc_15', 'ior_rah_05', 'ior_wmc_1', 'la_la_wa'],
+    ['ior_rc_15', 'ior_moc', 'ior_wmc_1', 'la_la_wa'],
+
+    ['ior_rah_05', 'ior_rc_075', 'ior_wmc_1', 'la_wh_wa'],
+    ['ior_moc', 'ior_rc_075', 'ior_wmc_1', 'la_wh_wa'],
+    ['ior_rac_05', 'ior_rh_075', 'ior_wmh_1', 'la_wh_wa'],
+    ['ior_moh', 'ior_rh_075', 'ior_wmh_1', 'la_wh_wa'],
+
+    ['ior_rah_05', 'ior_rc_1', 'ior_wmc_1', 'la_dr_wa'],
+    ['ior_moc', 'ior_rc_1', 'ior_wmc_1', 'la_dr_wa'],
+    ['ior_rac_05', 'ior_rh_1', 'ior_wmh_1', 'la_dr_wa'],
+    ['ior_moh', 'ior_rh_1', 'ior_wmh_1', 'la_dr_wa'],
+
+    ['ior_rah_05', 'ior_rc_125', 'ior_wmc_1', 'la_lh_wa'],
+    ['ior_moc', 'ior_rc_125', 'ior_wmc_1', 'la_lh_wa'],
+    ['ior_rac_05', 'ior_rh_125', 'ior_wmh_1', 'la_lh_wa'],
+    ['ior_moh', 'ior_rh_125', 'ior_wmh_1', 'la_lh_wa'],
+
+    ['ior_rah_075', 'ior_rc_1', 'ior_wmc_1', 'lh_dr_wa'],
+    ['ior_rac_075', 'ior_rh_1', 'ior_wmh_1', 'lh_dr_wa'],
+
+    ['ior_rh_125', 'ior_rac_1', 'ior_wmh_1', 'lh_dr_wa'],
+    ['ior_rc_125', 'ior_rah_1', 'ior_wmc_1', 'lh_dr_wa'],
+
+    ['ior_rh_125', 'ior_rac_075', 'ior_wmh_1', 'lh_lh_wa'],
+    ['ior_rc_125', 'ior_rah_075', 'ior_wmc_1', 'lh_lh_wa'],
+  ],
+  'A:2': [
+    ['ior_rh_25', 'ior_rac_225', 'ior_wmh_2', 'la_wh_wa'],
+    ['ior_rc_25', 'ior_rah_225', 'ior_wmc_2', 'la_wh_wa'],
+
+    ['ior_rh_25', 'ior_rac_2', 'ior_wmh_2', 'la_dr_wa'],
+    ['ior_rc_25', 'ior_rah_2', 'ior_wmc_2', 'la_dr_wa'],
+
+    ['ior_rh_25', 'ior_rac_175', 'ior_wmh_2', 'la_lh_wa'],
+    ['ior_rc_25', 'ior_rah_175', 'ior_wmc_2', 'la_lh_wa'],
+
+    ['ior_rh_25', 'ior_rac_15', 'ior_wmh_2', 'la_la_wa'],
+    ['ior_rc_25', 'ior_rah_15', 'ior_wmc_2', 'la_la_wa'],
+
+    ['ior_rah_15', 'ior_rc_175', 'ior_wmc_2', 'la_wh_wa'],
+    ['ior_rac_15', 'ior_rh_175', 'ior_wmh_2', 'la_wh_wa'],
+
+    ['ior_rah_15', 'ior_rc_2', 'ior_wmc_2', 'la_dr_wa'],
+    ['ior_rac_15', 'ior_rh_2', 'ior_wmh_2', 'la_dr_wa'],
+
+    ['ior_rah_15', 'ior_rc_225', 'ior_wmc_2', 'la_lh_wa'],
+    ['ior_rac_15', 'ior_rh_225', 'ior_wmh_2', 'la_lh_wa'],
+
+    ['ior_rah_175', 'ior_rc_2', 'ior_wmc_2', 'lh_dr_wa'],
+    ['ior_rac_175', 'ior_rh_2', 'ior_wmh_2', 'lh_dr_wa'],
+
+    ['ior_rh_225', 'ior_rac_2', 'ior_wmh_2', 'lh_dr_wa'],
+    ['ior_rc_225', 'ior_rah_2', 'ior_wmc_2', 'lh_dr_wa'],
+
+    ['ior_rh_225', 'ior_rac_175', 'ior_wmh_2', 'lh_lh_wa'],
+    ['ior_rc_225', 'ior_rah_175', 'ior_wmc_2', 'lh_lh_wa'],
+  ],
+  'A:3': [
+    ['ior_rh_35', 'ior_rac_325', 'ior_wmh_3', 'la_wh_wa'],
+    ['ior_rc_35', 'ior_rah_325', 'ior_wmc_3', 'la_wh_wa'],
+
+    ['ior_rh_35', 'ior_rac_3', 'ior_wmh_3', 'la_dr_wa'],
+    ['ior_rc_35', 'ior_rah_3', 'ior_wmc_3', 'la_dr_wa'],
+
+    ['ior_rh_35', 'ior_rac_275', 'ior_wmh_3', 'la_lh_wa'],
+    ['ior_rc_35', 'ior_rah_275', 'ior_wmc_3', 'la_lh_wa'],
+
+    ['ior_rh_35', 'ior_rac_25', 'ior_wmh_3', 'la_la_wa'],
+    ['ior_rc_35', 'ior_rah_25', 'ior_wmc_3', 'la_la_wa'],
+
+    ['ior_rah_25', 'ior_rc_275', 'ior_wmc_3', 'la_wh_wa'],
+    ['ior_rac_25', 'ior_rh_275', 'ior_wmh_3', 'la_wh_wa'],
+
+    ['ior_rah_25', 'ior_rc_3', 'ior_wmc_3', 'la_dr_wa'],
+    ['ior_rac_25', 'ior_rh_3', 'ior_wmh_3', 'la_dr_wa'],
+
+    ['ior_rah_25', 'ior_rc_325', 'ior_wmc_3', 'la_lh_wa'],
+    ['ior_rac_25', 'ior_rh_325', 'ior_wmh_3', 'la_lh_wa'],
+
+    ['ior_rah_275', 'ior_rc_3', 'ior_wmc_3', 'lh_dr_wa'],
+    ['ior_rac_275', 'ior_rh_3', 'ior_wmh_3', 'lh_dr_wa'],
+
+    ['ior_rh_325', 'ior_rac_3', 'ior_wmh_3', 'lh_dr_wa'],
+    ['ior_rc_325', 'ior_rah_3', 'ior_wmc_3', 'lh_dr_wa'],
+
+    ['ior_rh_325', 'ior_rac_275', 'ior_wmh_3', 'lh_lh_wa'],
+    ['ior_rc_325', 'ior_rah_275', 'ior_wmc_3', 'lh_lh_wa'],
+  ],
+  // 'A:4': [
+
+  //   ['ior_rh_45', 'ior_rac_425', 'ior_wmh_4', 'la_wh_wa'],
+  //   ['ior_rc_45', 'ior_rah_425', 'ior_wmc_4', 'la_wh_wa'],
+
+  //   ['ior_rh_45', 'ior_rac_4', 'ior_wmh_4', 'la_dr_wa'],
+  //   ['ior_rc_45', 'ior_rah_4', 'ior_wmc_4', 'la_dr_wa'],
+
+  //   ['ior_rh_45', 'ior_rac_375', 'ior_wmh_4', 'la_lh_wa'],
+  //   ['ior_rc_45', 'ior_rah_375', 'ior_wmc_4', 'la_lh_wa'],
+
+  //   ['ior_rh_45', 'ior_rac_35', 'ior_wmh_4', 'la_la_wa'],
+  //   ['ior_rc_45', 'ior_rah_35', 'ior_wmc_4', 'la_la_wa'],
+
+  //   ['ior_rah_35', 'ior_rc_375', 'ior_wmc_4', 'la_wh_wa'],
+  //   ['ior_rac_35', 'ior_rh_375', 'ior_wmh_4', 'la_wh_wa'],
+
+  //   ['ior_rah_35', 'ior_rc_4', 'ior_wmc_4', 'la_dr_wa'],
+  //   ['ior_rac_35', 'ior_rh_4', 'ior_wmh_4', 'la_dr_wa'],
+
+  //   ['ior_rah_35', 'ior_rc_425', 'ior_wmc_4', 'la_lh_wa'],
+  //   ['ior_rac_35', 'ior_rh_425', 'ior_wmh_4', 'la_lh_wa'],
+
+  //   ['ior_rah_375', 'ior_rc_4', 'ior_wmc_4', 'lh_dr_wa'],
+  //   ['ior_rac_375', 'ior_rh_4', 'ior_wmh_4', 'lh_dr_wa'],
+
+  //   ['ior_rh_425', 'ior_rac_4', 'ior_wmh_4', 'lh_dr_wa'],
+  //   ['ior_rc_425', 'ior_rah_4', 'ior_wmc_4', 'lh_dr_wa'],
+
+  //   ['ior_rh_425', 'ior_rac_375', 'ior_wmh_4', 'lh_lh_wa'],
+  //   ['ior_rc_425', 'ior_rah_375', 'ior_wmc_4', 'lh_lh_wa'],
+  // ],
+  // 'A:5': [
+  //   ['ior_rh_55', 'ior_rac_525', 'ior_wmh_5', 'la_wh_wa'],
+  //   ['ior_rc_55', 'ior_rah_525', 'ior_wmc_5', 'la_wh_wa'],
+
+  //   ['ior_rh_55', 'ior_rac_5', 'ior_wmh_5', 'la_dr_wa'],
+  //   ['ior_rc_55', 'ior_rah_5', 'ior_wmc_5', 'la_dr_wa'],
+
+  //   ['ior_rh_55', 'ior_rac_475', 'ior_wmh_5', 'la_lh_wa'],
+  //   ['ior_rc_55', 'ior_rah_475', 'ior_wmc_5', 'la_lh_wa'],
+
+  //   ['ior_rh_55', 'ior_rac_45', 'ior_wmh_5', 'la_la_wa'],
+  //   ['ior_rc_55', 'ior_rah_45', 'ior_wmc_5', 'la_la_wa'],
+
+  //   ['ior_rah_45', 'ior_rc_475', 'ior_wmc_5', 'la_wh_wa'],
+  //   ['ior_rac_45', 'ior_rh_475', 'ior_wmh_5', 'la_wh_wa'],
+
+  //   ['ior_rah_45', 'ior_rc_5', 'ior_wmc_5', 'la_dr_wa'],
+  //   ['ior_rac_45', 'ior_rh_5', 'ior_wmh_5', 'la_dr_wa'],
+
+  //   ['ior_rah_45', 'ior_rc_525', 'ior_wmc_5', 'la_lh_wa'],
+  //   ['ior_rac_45', 'ior_rh_525', 'ior_wmh_5', 'la_lh_wa'],
+
+  //   ['ior_rah_475', 'ior_rc_5', 'ior_wmc_5', 'lh_dr_wa'],
+  //   ['ior_rac_475', 'ior_rh_5', 'ior_wmh_5', 'lh_dr_wa'],
+
+  //   ['ior_rh_525', 'ior_rac_5', 'ior_wmh_5', 'lh_dr_wa'],
+  //   ['ior_rc_525', 'ior_rah_5', 'ior_wmc_5', 'lh_dr_wa'],
+
+  //   ['ior_rh_525', 'ior_rac_475', 'ior_wmh_5', 'lh_lh_wa'],
+  //   ['ior_rc_525', 'ior_rah_475', 'ior_wmc_5', 'lh_lh_wa'],
+  // ],
+
+
+  'D:1': [
+    ['ior_ouh_05', 'ior_ouc_075', 'ior_ot_1', 'la_wh_wa'],
+
+    ['ior_ouh_075', 'ior_ouc_1', 'ior_ot_1', 'lh_dr_wa'],
+
+    ['ior_ouc_125', 'ior_ouh_1', 'ior_ot_1', 'lh_dr_wa'],
+
+    ['ior_ouc_15', 'ior_ouh_125', 'ior_ot_1', 'la_wh_wa'],
+
+    ['ior_ouc_15', 'ior_ouh_1', 'ior_ot_1', 'la_dr_wa'],
+
+    ['ior_ouc_125', 'ior_ouh_075', 'ior_ot_1', 'lh_lh_wa'],
+
+    ['ior_ouh_05', 'ior_ouc_1', 'ior_ot_1', 'la_dr_wa'],
+
+    ['ior_ouh_05', 'ior_ouc_125', 'ior_ot_1', 'la_lh_wa'],
+
+    ['ior_ouc_15', 'ior_ouh_075', 'ior_ot_1', 'la_lh_wa'],
+
+    ['ior_ouc_15', 'ior_ouh_05', 'ior_ot_1', 'la_la_wa'],
+  ],
+  'D:2': [
+    ['ior_ouh_15', 'ior_ouc_175', 'ior_ot_2', 'la_wh_wa'],
+
+    ['ior_ouh_175', 'ior_ouc_2', 'ior_ot_2', 'lh_dr_wa'],
+
+    ['ior_ouc_225', 'ior_ouh_2', 'ior_ot_2', 'lh_dr_wa'],
+
+    ['ior_ouc_25', 'ior_ouh_225', 'ior_ot_2', 'la_wh_wa'],
+
+    ['ior_ouc_25', 'ior_ouh_2', 'ior_ot_2', 'la_dr_wa'],
+
+    ['ior_ouc_225', 'ior_ouh_175', 'ior_ot_2', 'lh_lh_wa'],
+
+    ['ior_ouh_15', 'ior_ouc_2', 'ior_ot_2', 'la_dr_wa'],
+
+    ['ior_ouh_15', 'ior_ouc_225', 'ior_ot_2', 'la_lh_wa'],
+
+    ['ior_ouc_25', 'ior_ouh_175', 'ior_ot_2', 'la_lh_wa'],
+
+    ['ior_ouc_25', 'ior_ouh_15', 'ior_ot_2', 'la_la_wa'],
+  ],
+  'D:3': [
+    ['ior_ouh_25', 'ior_ouc_275', 'ior_ot_3', 'la_wh_wa'],
+
+    ['ior_ouh_275', 'ior_ouc_3', 'ior_ot_3', 'lh_dr_wa'],
+
+    ['ior_ouc_325', 'ior_ouh_3', 'ior_ot_3', 'lh_dr_wa'],
+
+    ['ior_ouc_35', 'ior_ouh_325', 'ior_ot_3', 'la_wh_wa'],
+
+    ['ior_ouh_25', 'ior_ouc_3', 'ior_ot_3', 'la_dr_wa'],
+
+    ['ior_ouc_325', 'ior_ouh_275', 'ior_ot_3', 'lh_lh_wa'],
+
+    ['ior_ouc_35', 'ior_ouh_3', 'ior_ot_3', 'la_dr_wa'],
+
+    ['ior_ouh_25', 'ior_ouc_325', 'ior_ot_3', 'la_lh_wa'],
+
+    ['ior_ouc_35', 'ior_ouh_275', 'ior_ot_3', 'la_lh_wa'],
+
+    ['ior_ouc_35', 'ior_ouh_25', 'ior_ot_3', 'la_la_wa'],
+  ],
+  'D:4': [
+    ['ior_ouh_35', 'ior_ouc_375', 'ior_ot_4', 'la_wh_wa'],
+
+    ['ior_ouh_375', 'ior_ouc_4', 'ior_ot_4', 'lh_dr_wa'],
+
+    ['ior_ouc_425', 'ior_ouh_4', 'ior_ot_4', 'lh_dr_wa'],
+
+    ['ior_ouc_45', 'ior_ouh_425', 'ior_ot_4', 'la_wh_wa'],
+
+    ['ior_ouh_35', 'ior_ouc_4', 'ior_ot_4', 'la_dr_wa'],
+
+    ['ior_ouc_425', 'ior_ouh_375', 'ior_ot_4', 'lh_lh_wa'],
+
+    ['ior_ouc_45', 'ior_ouh_4', 'ior_ot_4', 'la_dr_wa'],
+
+    ['ior_ouh_35', 'ior_ouc_425', 'ior_ot_4', 'la_lh_wa'],
+
+    ['ior_ouc_45', 'ior_ouh_375', 'ior_ot_4', 'la_lh_wa'],
+
+    ['ior_ouc_45', 'ior_ouh_35', 'ior_ot_4', 'la_la_wa'],
+  ],
+  'D:5': [
+    ['ior_ouh_45', 'ior_ouc_475', 'ior_ot_5', 'la_wh_wa'],
+
+    ['ior_ouh_475', 'ior_ouc_5', 'ior_ot_5', 'lh_dr_wa'],
+
+    ['ior_ouc_525', 'ior_ouh_5', 'ior_ot_5', 'lh_dr_wa'],
+
+    ['ior_ouc_55', 'ior_ouh_525', 'ior_ot_5', 'la_wh_wa'],
+
+    ['ior_ouh_45', 'ior_ouc_5', 'ior_ot_5', 'la_dr_wa'],
+
+    ['ior_ouc_525', 'ior_ouh_475', 'ior_ot_5', 'lh_lh_wa'],
+
+    ['ior_ouc_55', 'ior_ouh_5', 'ior_ot_5', 'la_dr_wa'],
+
+    ['ior_ouh_45', 'ior_ouc_525', 'ior_ot_5', 'la_lh_wa'],
+
+    ['ior_ouc_55', 'ior_ouh_475', 'ior_ot_5', 'la_lh_wa'],
+
+    ['ior_ouc_55', 'ior_ouh_45', 'ior_ot_5', 'la_la_wa'],
+  ],
+  // 'D:6': [
+  //   ['ior_ouh_55', 'ior_ouc_575', 'ior_ot_6', 'la_wh_wa'],
+
+  //   ['ior_ouh_575', 'ior_ouc_6', 'ior_ot_6', 'lh_dr_wa'],
+
+  //   ['ior_ouc_625', 'ior_ouh_6', 'ior_ot_6', 'lh_dr_wa'],
+
+  //   ['ior_ouc_65', 'ior_ouh_625', 'ior_ot_6', 'la_wh_wa'],
+
+  //   ['ior_ouh_55', 'ior_ouc_6', 'ior_ot_6', 'la_dr_wa'],
+
+  //   ['ior_ouc_625', 'ior_ouh_575', 'ior_ot_6', 'lh_lh_wa'],
+
+  //   ['ior_ouc_65', 'ior_ouh_6', 'ior_ot_6', 'la_dr_wa'],
+
+  //   ['ior_ouh_55', 'ior_ouc_625', 'ior_ot_6', 'la_lh_wa'],
+
+  //   ['ior_ouc_65', 'ior_ouh_575', 'ior_ot_6', 'la_lh_wa'],
+
+  //   ['ior_ouc_65', 'ior_ouh_55', 'ior_ot_6', 'la_la_wa'],
+  // ],
+  // 'D:7': [
+  //   ['ior_ouh_65', 'ior_ouc_675', 'ior_ot_7', 'la_wh_wa'],
+
+  //   ['ior_ouh_675', 'ior_ouc_7', 'ior_ot_7', 'lh_dr_wa'],
+
+  //   ['ior_ouc_725', 'ior_ouh_7', 'ior_ot_7', 'lh_dr_wa'],
+
+  //   ['ior_ouc_75', 'ior_ouh_725', 'ior_ot_7', 'la_wh_wa'],
+
+  //   ['ior_ouh_65', 'ior_ouc_7', 'ior_ot_7', 'la_dr_wa'],
+
+  //   ['ior_ouc_725', 'ior_ouh_675', 'ior_ot_7', 'lh_lh_wa'],
+
+  //   ['ior_ouc_75', 'ior_ouh_7', 'ior_ot_7', 'la_dr_wa'],
+
+  //   ['ior_ouh_65', 'ior_ouc_725', 'ior_ot_7', 'la_lh_wa'],
+
+  //   ['ior_ouc_75', 'ior_ouh_675', 'ior_ot_7', 'la_lh_wa'],
+
+  //   ['ior_ouc_75', 'ior_ouh_65', 'ior_ot_7', 'la_la_wa'],
+  // ],
+  // 'G:0': [
+  //   ['ior_os_0-1', 'ior_ouc_35', 'ior_os_2-3', 'la_la_wa'],
+
+  //   ['ior_os_0-1', 'ior_ouc_225', 'ior_ot_2', 'la_lh_wa'],
+
+  //   ['ior_os_0-1', 'ior_ouc_25', 'ior_ot_2', 'la_la_wa'],
+
+  //   ['ior_os_0-1', 'ior_ouc_2', 'ior_ot_2', 'la_dr_wa'],
+  // ],
+  'R:0': [
+    ['ior_rh_05', 'ior_rac_05', '-', 'la_wa_rv'],
+    ['ior_rc_05', 'ior_rah_05', '-', 'la_wa_rv'],
+    ['ior_mh', 'ior_rac_05', '-', 'la_wa_rv'],
+    ['ior_mc', 'ior_rah_05', '-', 'la_wa_rv'],
+
+    ['ior_rh_15', 'ior_rac_15', '-', 'la_wa_rv'],
+    ['ior_rc_15', 'ior_rah_15', '-', 'la_wa_rv'],
+
+    ['ior_rh_25', 'ior_rac_25', '-', 'la_wa_rv'],
+    ['ior_rc_25', 'ior_rah_25', '-', 'la_wa_rv'],
+  ],
+  'P:0': [
+    ['ior_ouc_05', 'ior_ouh_05', '-', 'la_wa_rv'],
+    ['ior_ouc_15', 'ior_ouh_15', '-', 'la_wa_rv'],
+    ['ior_ouc_25', 'ior_ouh_25', '-', 'la_wa_rv'],
+    ['ior_ouc_35', 'ior_ouh_35', '-', 'la_wa_rv'],
+  ]
+}
+
+export default iorKeys;

+ 39 - 0
server/triangle/main.js

@@ -0,0 +1,39 @@
+import Logs from "../libs/logs.js";
+import ProcessData from "../libs/processData.js";
+import { getPassableEvents, eventsCombination } from "./trangleCalc.js";
+
+const processData = new ProcessData(process, 'triangle');
+// setInterval(() => {
+//   Logs.outDev('get games relations');
+//   processData.get('gamesRelations')
+//   .then(relations => {
+//     Logs.outDev('relations', relations);
+//   })
+//   .catch(error => {
+//     Logs.err('triangle error', error);
+//   });
+// }, 5000);
+
+const eventMatch = () => {
+  processData.get('gamesRelations')
+  .then(relations => {
+    if (!relations?.length) {
+      Logs.outDev('relation list is empty');
+    }
+
+    // Logs.outDev('relations', relations);
+
+    const passableEvents = getPassableEvents(relations);
+    const solutions = eventsCombination(passableEvents);
+    if (solutions?.length) {
+      processData.post('solutions', solutions);
+    }
+  })
+  .finally(() => {
+    setTimeout(() => {
+      eventMatch();
+    }, 2000);
+  });
+}
+
+eventMatch();

+ 156 - 0
server/triangle/trangleCalc.js

@@ -0,0 +1,156 @@
+import crypto from 'crypto';
+import iorKeys from './iorKeys.js';
+import eventSolutions from './eventSolutions.js';
+
+/**
+ * 精确浮点数字
+ * @param {number} number
+ * @param {number} x
+ * @returns {number}
+ */
+const fixFloat = (number, x=3) => {
+  return parseFloat(number.toFixed(x));
+}
+
+/**
+ * 盘口排序
+ */
+const priority = { polymarket: 1, pinnacle: 2 };
+const getPriority = (p) => {
+  return priority[p] ?? 99;
+}
+const sortCpr = (cpr) => {
+  const temp = [...cpr];
+  temp.sort((a, b) => getPriority(a.p) - getPriority(b.p));
+  return temp;
+}
+
+/**
+ * 获取平台类型
+ */
+const getPlatformKey = (cpr) => {
+  const platforms = sortCpr(cpr).map(item => item.p);
+  return [...new Set(platforms)].join('_');
+}
+
+const cartesianOdds = (selection) => {
+  const [a, b, c] = selection;
+  return a.flatMap(itemA => b.flatMap(itemB => c.map(itemC => [itemA, itemB, itemC])));
+}
+
+const getOptimalSelections = (odds, rules) => {
+  const results = [];
+
+  rules.forEach((rule, ruleIndex) => {
+    const validOptions = [];
+    const selection = [];
+    let isValid = true;
+    for (let i = 0; i < 3; i++) {
+      const key = rule[i];
+      let item = odds[key];
+      if (key === '-') {
+        item = { no: { v: 1 } };
+      }
+      if (!item) {
+        isValid = false;
+        break;
+      }
+      const candidates = Object.keys(item);
+      if (candidates.length === 0) {
+        isValid = false;
+        break;
+      }
+      selection.push(candidates.map(k => ({
+        k: key,
+        p: k,
+        o: item,
+        ...item[k],
+      })));
+    }
+    if (isValid) {
+      const cartesian = cartesianOdds(selection);
+      cartesian.forEach(iors => {
+        const iorsCount = new Set(iors.filter(ior => ior.p !== 'no').map(ior => ior.p));
+        if (iorsCount.size > 1) {
+          validOptions.push(iors);
+        }
+      });
+    }
+    validOptions.forEach(iors => {
+      results.push({ rule, iors, ruleIndex });
+    });
+  });
+  return results;
+}
+
+export const getPassableEvents = (relations) => {
+  return relations.map(({ platforms }) => {
+    const eventsMap = {};
+    const oddsMap = {};
+
+    Object.keys(platforms).forEach(platform => {
+      const { id, leagueName, teamHomeName, teamAwayName, timestamp, evtime, odds } = platforms[platform] ?? {};
+      if (!odds) {
+        return;
+      }
+      if (platform === 'polymarket') {
+        eventsMap['info'] = { id, leagueName, teamHomeName, teamAwayName, timestamp };
+      }
+      Object.keys(odds).forEach(ior => {
+        if (!oddsMap[ior]) {
+          oddsMap[ior] = {};
+        }
+        oddsMap[ior][platform] = odds[ior]
+      });
+    });
+    eventsMap['odds'] = oddsMap;
+    return eventsMap;
+  })
+  .filter(item => item?.info);
+}
+
+export const eventsCombination = (passableEvents) => {
+  const solutions = [];
+  passableEvents.forEach(events => {
+    const { odds, info } = events;
+    Object.keys(iorKeys).forEach(iorGroup => {
+      const rules = iorKeys[iorGroup];
+      const optimalSelections = getOptimalSelections(odds, rules);
+      optimalSelections.forEach(selection => {
+        const { iors, rule, ruleIndex } = selection;
+        const [, , , crossType] = rule;
+        const [ oddsSideA, oddsSideB, oddsSideC ] = iors;
+        const baseIndex = iors.reduce((minIdx, cur, idx) => cur.v < iors[minIdx].v ? idx : minIdx, 0);
+        if (!oddsSideA || !oddsSideB || !oddsSideC) {
+          return;
+        }
+        if (oddsSideA.v <= 1 || oddsSideB.v <= 1) {
+          return;
+        }
+        const cpr = [oddsSideA, oddsSideB, oddsSideC];
+        const betInfo = {
+          cross_type: crossType,
+          base_index: baseIndex,
+          base_stake: 10000,
+          odds_side_a: fixFloat(oddsSideA.v - 1),
+          odds_side_b: fixFloat(oddsSideB.v - 1),
+          odds_side_c: fixFloat(oddsSideC.v - 1),
+        };
+        const sol = eventSolutions(betInfo, true);
+        if (cpr[2].k == '-') {
+          cpr.pop();
+        }
+        if (!isNaN(sol?.win_average)) {
+          const id = info.id;
+          const keys = cpr.map(item => `${item.k}-${item.p}`).join('##');
+          const sid = crypto.createHash('sha1').update(`${id}-${keys}`).digest('hex');
+          const hasLower = cpr.some(item => item.q === 0);
+          const platformKey = getPlatformKey(cpr);
+          const timestamp = Date.now();
+          solutions.push({sid, sol, cpr, cross: platformKey, info, lower: hasLower, rule: `${iorGroup}:${ruleIndex}`, timestamp});
+        }
+      });
+    });
+  });
+  return solutions.sort((a, b) => b.sol.win_profit_rate - a.sol.win_profit_rate);
+}

+ 38 - 0
web/README.md

@@ -0,0 +1,38 @@
+# ppai-web
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
+
+## Recommended Browser Setup
+
+- Chromium-based browsers (Chrome, Edge, Brave, etc.):
+  - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
+  - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
+- Firefox:
+  - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
+  - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vite.dev/config/).
+
+## Project Setup
+
+```sh
+npm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+npm run dev
+```
+
+### Compile and Minify for Production
+
+```sh
+npm run build
+```

+ 13 - 0
web/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="viewport-fit=cover">
+    <title>Polymarket vs Pinnacle</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 8 - 0
web/jsconfig.json

@@ -0,0 +1,8 @@
+{
+  "compilerOptions": {
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"]
+}

+ 3550 - 0
web/package-lock.json

@@ -0,0 +1,3550 @@
+{
+  "name": "ppai-web",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "ppai-web",
+      "version": "0.0.0",
+      "dependencies": {
+        "ant-design-vue": "^4.2.6",
+        "axios": "^1.13.2",
+        "dayjs": "^1.11.19",
+        "vue": "^3.5.26",
+        "vue-router": "^4.6.4"
+      },
+      "devDependencies": {
+        "@vitejs/plugin-vue": "^6.0.3",
+        "sass": "^1.97.2",
+        "vite": "^7.3.0",
+        "vite-plugin-vue-devtools": "^8.0.5"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@ant-design/colors": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-6.0.0.tgz",
+      "integrity": "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^3.4.0"
+      }
+    },
+    "node_modules/@ant-design/icons-svg": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
+      "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
+      "license": "MIT"
+    },
+    "node_modules/@ant-design/icons-vue": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/@ant-design/icons-vue/-/icons-vue-7.0.1.tgz",
+      "integrity": "sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^6.0.0",
+        "@ant-design/icons-svg": "^4.2.1"
+      },
+      "peerDependencies": {
+        "vue": ">=3.0.3"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.28.6.tgz",
+      "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.6.tgz",
+      "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.6.tgz",
+      "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.28.6",
+        "@babel/generator": "^7.28.6",
+        "@babel/helper-compilation-targets": "^7.28.6",
+        "@babel/helper-module-transforms": "^7.28.6",
+        "@babel/helpers": "^7.28.6",
+        "@babel/parser": "^7.28.6",
+        "@babel/template": "^7.28.6",
+        "@babel/traverse": "^7.28.6",
+        "@babel/types": "^7.28.6",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.6.tgz",
+      "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.28.6",
+        "@babel/types": "^7.28.6",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-annotate-as-pure": {
+      "version": "7.27.3",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+      "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.27.3"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+      "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.28.6",
+        "@babel/helper-validator-option": "^7.27.1",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-create-class-features-plugin": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz",
+      "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.27.3",
+        "@babel/helper-member-expression-to-functions": "^7.28.5",
+        "@babel/helper-optimise-call-expression": "^7.27.1",
+        "@babel/helper-replace-supers": "^7.28.6",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+        "@babel/traverse": "^7.28.6",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+      "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-member-expression-to-functions": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz",
+      "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.28.5",
+        "@babel/types": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+      "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+      "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.28.6",
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "@babel/traverse": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-optimise-call-expression": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+      "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+      "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-replace-supers": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz",
+      "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-member-expression-to-functions": "^7.28.5",
+        "@babel/helper-optimise-call-expression": "^7.27.1",
+        "@babel/traverse": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+      "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.27.1",
+        "@babel/types": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+      "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.6.tgz",
+      "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.6.tgz",
+      "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.28.6"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-decorators": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.6.tgz",
+      "integrity": "sha512-RVdFPPyY9fCRAX68haPmOk2iyKW8PKJFthmm8NeSI3paNxKWGZIn99+VbIf0FrtCpFnPgnpF/L48tadi617ULg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-create-class-features-plugin": "^7.28.6",
+        "@babel/helper-plugin-utils": "^7.28.6",
+        "@babel/plugin-syntax-decorators": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-decorators": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz",
+      "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-import-attributes": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz",
+      "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-import-meta": {
+      "version": "7.10.4",
+      "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+      "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.10.4"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-jsx": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz",
+      "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-typescript": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz",
+      "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-typescript": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz",
+      "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.27.3",
+        "@babel/helper-create-class-features-plugin": "^7.28.6",
+        "@babel/helper-plugin-utils": "^7.28.6",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+        "@babel/plugin-syntax-typescript": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz",
+      "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz",
+      "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.28.6",
+        "@babel/parser": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.6.tgz",
+      "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.28.6",
+        "@babel/generator": "^7.28.6",
+        "@babel/helper-globals": "^7.28.0",
+        "@babel/parser": "^7.28.6",
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.28.6",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.6.tgz",
+      "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+      "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@emotion/hash": {
+      "version": "0.9.2",
+      "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz",
+      "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/unitless": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.8.1.tgz",
+      "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
+      "license": "MIT"
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
+      "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
+      "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
+      "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
+      "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
+      "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
+      "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
+      "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
+      "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
+      "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
+      "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
+      "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
+      "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
+      "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
+      "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
+      "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
+      "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
+      "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
+      "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
+      "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
+      "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
+      "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
+      "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
+      "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
+      "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
+      "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
+      "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@parcel/watcher": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.4.tgz",
+      "integrity": "sha512-WYa2tUVV5HiArWPB3ydlOc4R2ivq0IDrlqhMi3l7mVsFEXNcTfxYFPIHXHXIh/ca/y/V5N4E1zecyxdIBjYnkQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "detect-libc": "^2.0.3",
+        "is-glob": "^4.0.3",
+        "node-addon-api": "^7.0.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher-android-arm64": "2.5.4",
+        "@parcel/watcher-darwin-arm64": "2.5.4",
+        "@parcel/watcher-darwin-x64": "2.5.4",
+        "@parcel/watcher-freebsd-x64": "2.5.4",
+        "@parcel/watcher-linux-arm-glibc": "2.5.4",
+        "@parcel/watcher-linux-arm-musl": "2.5.4",
+        "@parcel/watcher-linux-arm64-glibc": "2.5.4",
+        "@parcel/watcher-linux-arm64-musl": "2.5.4",
+        "@parcel/watcher-linux-x64-glibc": "2.5.4",
+        "@parcel/watcher-linux-x64-musl": "2.5.4",
+        "@parcel/watcher-win32-arm64": "2.5.4",
+        "@parcel/watcher-win32-ia32": "2.5.4",
+        "@parcel/watcher-win32-x64": "2.5.4"
+      }
+    },
+    "node_modules/@parcel/watcher-android-arm64": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.4.tgz",
+      "integrity": "sha512-hoh0vx4v+b3BNI7Cjoy2/B0ARqcwVNrzN/n7DLq9ZB4I3lrsvhrkCViJyfTj/Qi5xM9YFiH4AmHGK6pgH1ss7g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-arm64": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.4.tgz",
+      "integrity": "sha512-kphKy377pZiWpAOyTgQYPE5/XEKVMaj6VUjKT5VkNyUJlr2qZAn8gIc7CPzx+kbhvqHDT9d7EqdOqRXT6vk0zw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-x64": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.4.tgz",
+      "integrity": "sha512-UKaQFhCtNJW1A9YyVz3Ju7ydf6QgrpNQfRZ35wNKUhTQ3dxJ/3MULXN5JN/0Z80V/KUBDGa3RZaKq1EQT2a2gg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-freebsd-x64": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.4.tgz",
+      "integrity": "sha512-Dib0Wv3Ow/m2/ttvLdeI2DBXloO7t3Z0oCp4bAb2aqyqOjKPPGrg10pMJJAQ7tt8P4V2rwYwywkDhUia/FgS+Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-glibc": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.4.tgz",
+      "integrity": "sha512-I5Vb769pdf7Q7Sf4KNy8Pogl/URRCKu9ImMmnVKYayhynuyGYMzuI4UOWnegQNa2sGpsPSbzDsqbHNMyeyPCgw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-musl": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.4.tgz",
+      "integrity": "sha512-kGO8RPvVrcAotV4QcWh8kZuHr9bXi9a3bSZw7kFarYR0+fGliU7hd/zevhjw8fnvIKG3J9EO5G6sXNGCSNMYPQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-glibc": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.4.tgz",
+      "integrity": "sha512-KU75aooXhqGFY2W5/p8DYYHt4hrjHZod8AhcGAmhzPn/etTa+lYCDB2b1sJy3sWJ8ahFVTdy+EbqSBvMx3iFlw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-musl": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.4.tgz",
+      "integrity": "sha512-Qx8uNiIekVutnzbVdrgSanM+cbpDD3boB1f8vMtnuG5Zau4/bdDbXyKwIn0ToqFhIuob73bcxV9NwRm04/hzHQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-glibc": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.4.tgz",
+      "integrity": "sha512-UYBQvhYmgAv61LNUn24qGQdjtycFBKSK3EXr72DbJqX9aaLbtCOO8+1SkKhD/GNiJ97ExgcHBrukcYhVjrnogA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-musl": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.4.tgz",
+      "integrity": "sha512-YoRWCVgxv8akZrMhdyVi6/TyoeeMkQ0PGGOf2E4omODrvd1wxniXP+DBynKoHryStks7l+fDAMUBRzqNHrVOpg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-arm64": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.4.tgz",
+      "integrity": "sha512-iby+D/YNXWkiQNYcIhg8P5hSjzXEHaQrk2SLrWOUD7VeC4Ohu0WQvmV+HDJokZVJ2UjJ4AGXW3bx7Lls9Ln4TQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-ia32": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.4.tgz",
+      "integrity": "sha512-vQN+KIReG0a2ZDpVv8cgddlf67J8hk1WfZMMP7sMeZmJRSmEax5xNDNWKdgqSe2brOKTQQAs3aCCUal2qBHAyg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-x64": {
+      "version": "2.5.4",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.4.tgz",
+      "integrity": "sha512-3A6efb6BOKwyw7yk9ro2vus2YTt2nvcd56AuzxdMiVOxL9umDyN5PKkKfZ/gZ9row41SjVmTVQNWQhaRRGpOKw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@polka/url": {
+      "version": "1.0.0-next.29",
+      "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz",
+      "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-beta.53",
+      "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
+      "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
+      "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
+      "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
+      "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
+      "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
+      "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
+      "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
+      "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
+      "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
+      "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
+      "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
+      "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
+      "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
+      "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
+      "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
+      "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
+      "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
+      "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
+      "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
+      "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
+      "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
+      "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
+      "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
+      "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
+      "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
+      "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@simonwep/pickr": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmmirror.com/@simonwep/pickr/-/pickr-1.8.2.tgz",
+      "integrity": "sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==",
+      "license": "MIT",
+      "dependencies": {
+        "core-js": "^3.15.1",
+        "nanopop": "^2.1.0"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz",
+      "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "1.0.0-beta.53"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vue/babel-helper-vue-transform-on": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz",
+      "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vue/babel-plugin-jsx": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz",
+      "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.27.1",
+        "@babel/helper-plugin-utils": "^7.27.1",
+        "@babel/plugin-syntax-jsx": "^7.27.1",
+        "@babel/template": "^7.27.2",
+        "@babel/traverse": "^7.28.0",
+        "@babel/types": "^7.28.2",
+        "@vue/babel-helper-vue-transform-on": "1.5.0",
+        "@vue/babel-plugin-resolve-type": "1.5.0",
+        "@vue/shared": "^3.5.18"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "@babel/core": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/babel-plugin-resolve-type": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz",
+      "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.27.1",
+        "@babel/helper-module-imports": "^7.27.1",
+        "@babel/helper-plugin-utils": "^7.27.1",
+        "@babel/parser": "^7.28.0",
+        "@vue/compiler-sfc": "^3.5.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.27",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.27.tgz",
+      "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.28.5",
+        "@vue/shared": "3.5.27",
+        "entities": "^7.0.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.27",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz",
+      "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.27",
+        "@vue/shared": "3.5.27"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.27",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz",
+      "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.28.5",
+        "@vue/compiler-core": "3.5.27",
+        "@vue/compiler-dom": "3.5.27",
+        "@vue/compiler-ssr": "3.5.27",
+        "@vue/shared": "3.5.27",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.6",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.27",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz",
+      "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.27",
+        "@vue/shared": "3.5.27"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/devtools-core": {
+      "version": "8.0.5",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-8.0.5.tgz",
+      "integrity": "sha512-dpCw8nl0GDBuiL9SaY0mtDxoGIEmU38w+TQiYEPOLhW03VDC0lfNMYXS/qhl4I0YlysGp04NLY4UNn6xgD0VIQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-kit": "^8.0.5",
+        "@vue/devtools-shared": "^8.0.5",
+        "mitt": "^3.0.1",
+        "nanoid": "^5.1.5",
+        "pathe": "^2.0.3",
+        "vite-hot-client": "^2.1.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/@vue/devtools-core/node_modules/nanoid": {
+      "version": "5.1.6",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.6.tgz",
+      "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.js"
+      },
+      "engines": {
+        "node": "^18 || >=20"
+      }
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "8.0.5",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz",
+      "integrity": "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^8.0.5",
+        "birpc": "^2.6.1",
+        "hookable": "^5.5.3",
+        "mitt": "^3.0.1",
+        "perfect-debounce": "^2.0.0",
+        "speakingurl": "^14.0.1",
+        "superjson": "^2.2.2"
+      }
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "8.0.5",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz",
+      "integrity": "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "rfdc": "^1.4.1"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.27",
+      "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.27.tgz",
+      "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.27"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.27",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.27.tgz",
+      "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.27",
+        "@vue/shared": "3.5.27"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.27",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz",
+      "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.27",
+        "@vue/runtime-core": "3.5.27",
+        "@vue/shared": "3.5.27",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.27",
+      "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.27.tgz",
+      "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.27",
+        "@vue/shared": "3.5.27"
+      },
+      "peerDependencies": {
+        "vue": "3.5.27"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.27",
+      "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.27.tgz",
+      "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==",
+      "license": "MIT"
+    },
+    "node_modules/ansis": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/ansis/-/ansis-4.2.0.tgz",
+      "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/ant-design-vue": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmmirror.com/ant-design-vue/-/ant-design-vue-4.2.6.tgz",
+      "integrity": "sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^6.0.0",
+        "@ant-design/icons-vue": "^7.0.0",
+        "@babel/runtime": "^7.10.5",
+        "@ctrl/tinycolor": "^3.5.0",
+        "@emotion/hash": "^0.9.0",
+        "@emotion/unitless": "^0.8.0",
+        "@simonwep/pickr": "~1.8.0",
+        "array-tree-filter": "^2.1.0",
+        "async-validator": "^4.0.0",
+        "csstype": "^3.1.1",
+        "dayjs": "^1.10.5",
+        "dom-align": "^1.12.1",
+        "dom-scroll-into-view": "^2.0.0",
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.15",
+        "resize-observer-polyfill": "^1.5.1",
+        "scroll-into-view-if-needed": "^2.2.25",
+        "shallow-equal": "^1.0.0",
+        "stylis": "^4.1.3",
+        "throttle-debounce": "^5.0.0",
+        "vue-types": "^3.0.0",
+        "warning": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=12.22.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/ant-design-vue"
+      },
+      "peerDependencies": {
+        "vue": ">=3.2.0"
+      }
+    },
+    "node_modules/array-tree-filter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/array-tree-filter/-/array-tree-filter-2.1.0.tgz",
+      "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==",
+      "license": "MIT"
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz",
+      "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.4",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.9.15",
+      "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz",
+      "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.js"
+      }
+    },
+    "node_modules/birpc": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz",
+      "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.1",
+      "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz",
+      "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "baseline-browser-mapping": "^2.9.0",
+        "caniuse-lite": "^1.0.30001759",
+        "electron-to-chromium": "^1.5.263",
+        "node-releases": "^2.0.27",
+        "update-browserslist-db": "^1.2.0"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/bundle-name": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz",
+      "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "run-applescript": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001765",
+      "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz",
+      "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chokidar": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
+      "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/compute-scroll-into-view": {
+      "version": "1.0.20",
+      "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
+      "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
+      "license": "MIT"
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/copy-anything": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz",
+      "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-what": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/core-js": {
+      "version": "3.47.0",
+      "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.47.0.tgz",
+      "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.19",
+      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz",
+      "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/default-browser": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.4.0.tgz",
+      "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "bundle-name": "^4.1.0",
+        "default-browser-id": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/default-browser-id": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz",
+      "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/define-lazy-prop": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+      "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dom-align": {
+      "version": "1.12.4",
+      "resolved": "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz",
+      "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==",
+      "license": "MIT"
+    },
+    "node_modules/dom-scroll-into-view": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz",
+      "integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==",
+      "license": "MIT"
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.267",
+      "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
+      "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/entities": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.0.tgz",
+      "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/error-stack-parser-es": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
+      "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.2.tgz",
+      "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.27.2",
+        "@esbuild/android-arm": "0.27.2",
+        "@esbuild/android-arm64": "0.27.2",
+        "@esbuild/android-x64": "0.27.2",
+        "@esbuild/darwin-arm64": "0.27.2",
+        "@esbuild/darwin-x64": "0.27.2",
+        "@esbuild/freebsd-arm64": "0.27.2",
+        "@esbuild/freebsd-x64": "0.27.2",
+        "@esbuild/linux-arm": "0.27.2",
+        "@esbuild/linux-arm64": "0.27.2",
+        "@esbuild/linux-ia32": "0.27.2",
+        "@esbuild/linux-loong64": "0.27.2",
+        "@esbuild/linux-mips64el": "0.27.2",
+        "@esbuild/linux-ppc64": "0.27.2",
+        "@esbuild/linux-riscv64": "0.27.2",
+        "@esbuild/linux-s390x": "0.27.2",
+        "@esbuild/linux-x64": "0.27.2",
+        "@esbuild/netbsd-arm64": "0.27.2",
+        "@esbuild/netbsd-x64": "0.27.2",
+        "@esbuild/openbsd-arm64": "0.27.2",
+        "@esbuild/openbsd-x64": "0.27.2",
+        "@esbuild/openharmony-arm64": "0.27.2",
+        "@esbuild/sunos-x64": "0.27.2",
+        "@esbuild/win32-arm64": "0.27.2",
+        "@esbuild/win32-ia32": "0.27.2",
+        "@esbuild/win32-x64": "0.27.2"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/immutable": {
+      "version": "5.1.4",
+      "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.4.tgz",
+      "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/is-docker": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz",
+      "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "is-docker": "cli.js"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-inside-container": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz",
+      "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-docker": "^3.0.0"
+      },
+      "bin": {
+        "is-inside-container": "cli.js"
+      },
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-plain-object": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-3.0.1.tgz",
+      "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-what": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz",
+      "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/is-wsl": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.0.tgz",
+      "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-inside-container": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/kolorist": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz",
+      "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-es": {
+      "version": "4.17.22",
+      "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.22.tgz",
+      "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
+      "license": "MIT"
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/mrmime": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz",
+      "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/nanopop": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.4.2.tgz",
+      "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==",
+      "license": "MIT"
+    },
+    "node_modules/node-addon-api": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz",
+      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.27",
+      "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz",
+      "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/ohash": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz",
+      "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/open": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz",
+      "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "default-browser": "^5.2.1",
+        "define-lazy-prop": "^3.0.0",
+        "is-inside-container": "^1.0.0",
+        "wsl-utils": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/perfect-debounce": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.0.0.tgz",
+      "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
+      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.6",
+      "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
+    "node_modules/readdirp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz",
+      "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.18.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+      "license": "MIT"
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/rollup": {
+      "version": "4.55.1",
+      "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.55.1.tgz",
+      "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.55.1",
+        "@rollup/rollup-android-arm64": "4.55.1",
+        "@rollup/rollup-darwin-arm64": "4.55.1",
+        "@rollup/rollup-darwin-x64": "4.55.1",
+        "@rollup/rollup-freebsd-arm64": "4.55.1",
+        "@rollup/rollup-freebsd-x64": "4.55.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.55.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.55.1",
+        "@rollup/rollup-linux-arm64-musl": "4.55.1",
+        "@rollup/rollup-linux-loong64-gnu": "4.55.1",
+        "@rollup/rollup-linux-loong64-musl": "4.55.1",
+        "@rollup/rollup-linux-ppc64-gnu": "4.55.1",
+        "@rollup/rollup-linux-ppc64-musl": "4.55.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.55.1",
+        "@rollup/rollup-linux-riscv64-musl": "4.55.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.55.1",
+        "@rollup/rollup-linux-x64-gnu": "4.55.1",
+        "@rollup/rollup-linux-x64-musl": "4.55.1",
+        "@rollup/rollup-openbsd-x64": "4.55.1",
+        "@rollup/rollup-openharmony-arm64": "4.55.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.55.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.55.1",
+        "@rollup/rollup-win32-x64-gnu": "4.55.1",
+        "@rollup/rollup-win32-x64-msvc": "4.55.1",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/run-applescript": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz",
+      "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/sass": {
+      "version": "1.97.2",
+      "resolved": "https://registry.npmmirror.com/sass/-/sass-1.97.2.tgz",
+      "integrity": "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^4.0.0",
+        "immutable": "^5.0.2",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher": "^2.4.1"
+      }
+    },
+    "node_modules/scroll-into-view-if-needed": {
+      "version": "2.2.31",
+      "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
+      "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
+      "license": "MIT",
+      "dependencies": {
+        "compute-scroll-into-view": "^1.0.20"
+      }
+    },
+    "node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/shallow-equal": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-1.2.1.tgz",
+      "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==",
+      "license": "MIT"
+    },
+    "node_modules/sirv": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/sirv/-/sirv-3.0.2.tgz",
+      "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@polka/url": "^1.0.0-next.24",
+        "mrmime": "^2.0.0",
+        "totalist": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/speakingurl": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
+      "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/stylis": {
+      "version": "4.3.6",
+      "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz",
+      "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
+      "license": "MIT"
+    },
+    "node_modules/superjson": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz",
+      "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "copy-anything": "^4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/throttle-debounce": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
+      "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.22"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.15",
+      "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
+      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/totalist": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz",
+      "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/unplugin-utils": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
+      "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/vite": {
+      "version": "7.3.1",
+      "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz",
+      "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.27.0",
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3",
+        "postcss": "^8.5.6",
+        "rollup": "^4.43.0",
+        "tinyglobby": "^0.2.15"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "lightningcss": "^1.21.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite-dev-rpc": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz",
+      "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "birpc": "^2.4.0",
+        "vite-hot-client": "^2.1.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0"
+      }
+    },
+    "node_modules/vite-hot-client": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/vite-hot-client/-/vite-hot-client-2.1.0.tgz",
+      "integrity": "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0"
+      }
+    },
+    "node_modules/vite-plugin-inspect": {
+      "version": "11.3.3",
+      "resolved": "https://registry.npmmirror.com/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz",
+      "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansis": "^4.1.0",
+        "debug": "^4.4.1",
+        "error-stack-parser-es": "^1.0.5",
+        "ohash": "^2.0.11",
+        "open": "^10.2.0",
+        "perfect-debounce": "^2.0.0",
+        "sirv": "^3.0.1",
+        "unplugin-utils": "^0.3.0",
+        "vite-dev-rpc": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vite": "^6.0.0 || ^7.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "@nuxt/kit": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite-plugin-vue-devtools": {
+      "version": "8.0.5",
+      "resolved": "https://registry.npmmirror.com/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.0.5.tgz",
+      "integrity": "sha512-p619BlKFOqQXJ6uDWS1vUPQzuJOD6xJTfftj57JXBGoBD/yeQCowR7pnWcr/FEX4/HVkFbreI6w2uuGBmQOh6A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-core": "^8.0.5",
+        "@vue/devtools-kit": "^8.0.5",
+        "@vue/devtools-shared": "^8.0.5",
+        "sirv": "^3.0.2",
+        "vite-plugin-inspect": "^11.3.3",
+        "vite-plugin-vue-inspector": "^5.3.2"
+      },
+      "engines": {
+        "node": ">=v14.21.3"
+      },
+      "peerDependencies": {
+        "vite": "^6.0.0 || ^7.0.0-0"
+      }
+    },
+    "node_modules/vite-plugin-vue-inspector": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmmirror.com/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.2.tgz",
+      "integrity": "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.23.0",
+        "@babel/plugin-proposal-decorators": "^7.23.0",
+        "@babel/plugin-syntax-import-attributes": "^7.22.5",
+        "@babel/plugin-syntax-import-meta": "^7.10.4",
+        "@babel/plugin-transform-typescript": "^7.22.15",
+        "@vue/babel-plugin-jsx": "^1.1.5",
+        "@vue/compiler-dom": "^3.3.4",
+        "kolorist": "^1.8.0",
+        "magic-string": "^0.30.4"
+      },
+      "peerDependencies": {
+        "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0"
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.27",
+      "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.27.tgz",
+      "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.27",
+        "@vue/compiler-sfc": "3.5.27",
+        "@vue/runtime-dom": "3.5.27",
+        "@vue/server-renderer": "3.5.27",
+        "@vue/shared": "3.5.27"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz",
+      "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/vue-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/vue-types/-/vue-types-3.0.2.tgz",
+      "integrity": "sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==",
+      "license": "MIT",
+      "dependencies": {
+        "is-plain-object": "3.0.1"
+      },
+      "engines": {
+        "node": ">=10.15.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/warning": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz",
+      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.0.0"
+      }
+    },
+    "node_modules/wsl-utils": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz",
+      "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-wsl": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    }
+  }
+}

+ 27 - 0
web/package.json

@@ -0,0 +1,27 @@
+{
+  "name": "ppai-web",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "engines": {
+    "node": "^20.19.0 || >=22.12.0"
+  },
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "ant-design-vue": "^4.2.6",
+    "axios": "^1.13.2",
+    "dayjs": "^1.11.19",
+    "vue": "^3.5.26",
+    "vue-router": "^4.6.4"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^6.0.3",
+    "sass": "^1.97.2",
+    "vite": "^7.3.0",
+    "vite-plugin-vue-devtools": "^8.0.5"
+  }
+}

BIN
web/public/favicon.ico


+ 11 - 0
web/src/main.js

@@ -0,0 +1,11 @@
+import { createApp } from 'vue';
+import antd from 'ant-design-vue';
+import router from './router';
+import main from '@/main.vue';
+
+import 'ant-design-vue/dist/reset.css';
+
+const app = createApp(main);
+app.use(antd);
+app.use(router);
+app.mount('#app');

+ 57 - 0
web/src/main.vue

@@ -0,0 +1,57 @@
+<script setup>
+import { ref, watch } from 'vue';
+import { RouterView, useRoute, useRouter } from 'vue-router';
+import { AccountBookOutlined, TeamOutlined, TrophyOutlined, BookOutlined } from '@ant-design/icons-vue';
+
+// 获取当前路由和路由实例
+const route = useRoute();
+const router = useRouter();
+const current = ref([route.name]);
+
+// 监听路由变化,更新选中菜单
+watch(() => route.name, (newName) => {
+  current.value = [newName];
+});
+
+// 处理菜单点击事件
+const handleMenuClick = (e) => {
+  const key = e.key;
+  router.push({ name: key });
+};
+
+</script>
+
+<template>
+  <a-menu v-model:selectedKeys="current" mode="horizontal" @click="handleMenuClick">
+    <a-menu-item key="home">
+      <template #icon>
+        <account-book-outlined />
+      </template>
+      策略
+    </a-menu-item>
+    <a-menu-item key="leagues">
+      <template #icon>
+        <team-outlined />
+      </template>
+      联赛
+    </a-menu-item>
+    <a-menu-item key="games">
+      <template #icon>
+        <trophy-outlined />
+      </template>
+      比赛
+    </a-menu-item>
+    <a-menu-item key="locales">
+      <template #icon>
+        <book-outlined />
+      </template>
+      翻译
+    </a-menu-item>
+  </a-menu>
+
+  <router-view />
+
+</template>
+
+<style scoped>
+</style>

+ 37 - 0
web/src/router/index.js

@@ -0,0 +1,37 @@
+import { createRouter, createWebHistory } from 'vue-router';
+import HomeView from '@/views/home.vue';
+import GamesView from '@/views/games.vue';
+import LeaguesView from '@/views/leagues.vue';
+import LocalesView from '@/views/locales.vue';
+
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: '/',
+      redirect: '/home'
+    },
+    {
+      path: '/home',
+      name: 'home',
+      component: HomeView
+    },
+    {
+      path: '/games',
+      name: 'games',
+      component: GamesView
+    },
+    {
+      path: '/leagues',
+      name: 'leagues',
+      component: LeaguesView
+    },
+    {
+      path: '/locales',
+      name: 'locales',
+      component: LocalesView
+    },
+  ],
+});
+
+export default router;

+ 364 - 0
web/src/views/games.vue

@@ -0,0 +1,364 @@
+<script setup>
+import axios from 'axios';
+import dayjs from 'dayjs';
+import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue';
+import { message } from 'ant-design-vue';
+import { ReloadOutlined, PlusOutlined, LinkOutlined, DisconnectOutlined } from '@ant-design/icons-vue';
+
+const search = ref('');
+const games = ref(null);
+const gamesRelations = ref([]);
+const selectedGames = reactive({
+  polymarket: null,
+  pinnacle: null,
+});
+
+const onSearch = (value) => {
+  search.value = value;
+};
+
+const updateGames = async () => {
+  return axios.get('/api/games/get_games').then(res => {
+    if (res.data.statusCode === 200) {
+      games.value = res.data.data;
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  });
+};
+
+const selectGame = (type, game) => {
+  const { id, leagueId, leagueName, teamHomeName, teamAwayName, timestamp } = game;
+  const currentGames = { id, leagueId, leagueName, teamHomeName, teamAwayName, timestamp };
+  if (type === 'polymarket') {
+    if (selectedGames.polymarket?.id === id) {
+      selectedGames.polymarket = null;
+    }
+    else {
+      selectedGames.polymarket = currentGames;
+    }
+  }
+  else if (type === 'pinnacle') {
+    if (selectedGames.pinnacle?.id === id) {
+      selectedGames.pinnacle = null;
+    }
+    else {
+      selectedGames.pinnacle = currentGames;
+    }
+  }
+  else {
+    throw new Error('invalid type');
+  }
+}
+
+const resetSelectedGames = () => {
+  selectedGames.polymarket = null;
+  selectedGames.pinnacle = null;
+}
+
+const setGamesRelation = () => {
+  if (!selectedGames.polymarket || !selectedGames.pinnacle) {
+    message.error('请选择要添加的比赛');
+    return;
+  }
+  const gameRelation = {
+    id: selectedGames.polymarket.id,
+    platforms: selectedGames,
+    timestamp: selectedGames.polymarket.timestamp,
+  };
+  axios.post('/api/games/set_relation', gameRelation)
+  .then(res => {
+    if (res.data.statusCode === 200) {
+      message.success('添加成功');
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+    resetSelectedGames();
+    return refresh();
+  })
+  .then(() => {
+    if (search.value.trim() && (!polymarketGames.value.length || !pinnacleGames.value.length)) {
+      search.value = '';
+    }
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  });
+};
+
+const removeGamesRelation = (relation) => {
+  axios.post('/api/games/remove_relation', { id: relation.id })
+  .then(res => {
+    if (res.data.statusCode === 200) {
+      message.success('删除成功');
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+    return refresh();
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  });
+}
+
+const updateGamesRelations = async () => {
+  return axios.get('/api/games/get_relations')
+  .then(res => {
+    if (res.data.statusCode === 200) {
+      gamesRelations.value = res.data.data;
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  });
+};
+
+const refresh = async () => {
+  return Promise.all([updateGames(), updateGamesRelations()]);
+}
+
+const getGames = (platform) => {
+  const searchValue = search.value.trim().toLowerCase();
+  return games.value?.[platform].map(item => {
+    const { id, timestamp } = item;
+    let selected = false;
+    let disabeld = false;
+    if (selectedGames[platform]?.id === id) {
+      selected = true;
+    }
+    if (gamesRelations.value.some(relation => relation.platforms[platform]?.id === id)) {
+      disabeld = true;
+    }
+    const dateTime = dayjs(timestamp).format('MM-DD HH:mm');
+    return { ...item, selected, disabeld, dateTime };
+  }).filter(item => {
+    const { disabeld, leagueName, teamHomeName, teamAwayName, localesLeagueName, localesTeamHomeName, localesTeamAwayName } = item;
+    return !disabeld && (!searchValue
+      || leagueName?.toLowerCase().includes(searchValue)
+      || localesLeagueName?.toLowerCase().includes(searchValue)
+      || teamHomeName?.toLowerCase().includes(searchValue)
+      || localesTeamHomeName?.toLowerCase().includes(searchValue)
+      || teamAwayName?.toLowerCase().includes(searchValue)
+      || localesTeamAwayName?.toLowerCase().includes(searchValue));
+  }) ?? [];
+}
+
+const polymarketGames = computed(() => {
+  return getGames('polymarket');
+});
+
+const pinnacleGames = computed(() => {
+  return getGames('pinnacle');
+});
+
+const gamesRelationsFiltered = computed(() => {
+  const searchValue = search.value.trim().toLowerCase();
+  return gamesRelations.value.filter(item => {
+    const { platforms: { polymarket, pinnacle } } = item;
+    return !searchValue
+    || polymarket.leagueName?.toLowerCase().includes(searchValue)
+    || polymarket.localesLeagueName?.toLowerCase().includes(searchValue)
+    || polymarket.teamHomeName?.toLowerCase().includes(searchValue)
+    || polymarket.localesTeamHomeName?.toLowerCase().includes(searchValue)
+    || polymarket.teamAwayName?.toLowerCase().includes(searchValue)
+    || polymarket.localesTeamAwayName?.toLowerCase().includes(searchValue)
+    || pinnacle.leagueName?.toLowerCase().includes(searchValue)
+    || pinnacle.teamHomeName?.toLowerCase().includes(searchValue)
+    || pinnacle.teamAwayName?.toLowerCase().includes(searchValue);
+  }) ?? [];
+});
+
+onMounted(() => {
+  refresh();
+});
+</script>
+
+<template>
+  <a-page-header title="比赛">
+    <template #extra>
+      <a-input-search
+        v-model:value="search"
+        placeholder="搜索"
+        :allowClear="true"
+        @search="onSearch"
+      />
+      <a-button @click="refresh">
+        <template #icon>
+          <reload-outlined />
+        </template>
+        刷新
+      </a-button>
+      <a-button type="primary" @click="setGamesRelation">
+        <template #icon>
+          <plus-outlined />
+        </template>
+        添加
+      </a-button>
+    </template>
+  </a-page-header>
+
+  <div class="games-container">
+    <div class="games-column polymarket">
+      <h3>Polymarket 比赛 ({{ polymarketGames.length }})</h3>
+      <a-list item-layout="horizontal" :data-source="polymarketGames" size="small">
+        <template #renderItem="{ item }">
+          <a-list-item :class="{ selected: item.selected, disabled: item.disabeld }" @click="!item.disabeld && selectGame('polymarket', item)">
+            <div class="game-info">
+              <div class="game-league-name">{{ item.localesLeagueName }} <em>{{ item.leagueName }}</em></div>
+              <div class="game-team-name home-team-name">{{ item.localesTeamHomeName }} <em>{{ item.teamHomeName }}</em></div>
+              <div class="game-team-name away-team-name">{{ item.localesTeamAwayName }} <em>{{ item.teamAwayName }}</em></div>
+            </div>
+            <div class="game-date-time">{{ item.dateTime }}</div>
+          </a-list-item>
+        </template>
+      </a-list>
+    </div>
+    <div class="games-column pinnacle">
+      <h3>Pinnacle 比赛 ({{ pinnacleGames.length }})</h3>
+      <a-list item-layout="horizontal" :data-source="pinnacleGames" size="small">
+        <template #renderItem="{ item }">
+          <a-list-item :class="{ selected: item.selected, disabled: item.disabeld }" @click="!item.disabeld && selectGame('pinnacle', item)">
+            <div class="game-info">
+              <div class="game-league-name">{{ item.leagueName }}</div>
+              <div class="game-team-name home-team-name">{{ item.teamHomeName }}</div>
+              <div class="game-team-name away-team-name">{{ item.teamAwayName }}</div>
+            </div>
+            <div class="game-date-time">{{ item.dateTime }}</div>
+          </a-list-item>
+        </template>
+      </a-list>
+    </div>
+    <div class="games-column relations">
+      <h3>已添加的比赛 ({{ gamesRelationsFiltered.length }})</h3>
+      <a-list item-layout="horizontal" :data-source="gamesRelationsFiltered" size="small">
+        <template #renderItem="{ item }">
+          <a-list-item>
+            <div class="game-info">
+              <div class="game-league-name">{{ item.platforms.polymarket.localesLeagueName }} <em>{{ item.platforms.polymarket.leagueName }}</em></div>
+              <div class="game-team-name home-team-name">{{ item.platforms.polymarket.localesTeamHomeName }} <em>{{ item.platforms.polymarket.teamHomeName }}</em></div>
+              <div class="game-team-name away-team-name">{{ item.platforms.polymarket.localesTeamAwayName }} <em>{{ item.platforms.polymarket.teamAwayName }}</em></div>
+            </div>
+            <div class="game-info">
+              <div class="game-league-name">{{ item.platforms.pinnacle.leagueName }}</div>
+              <div class="game-team-name home-team-name">{{ item.platforms.pinnacle.teamHomeName }}</div>
+              <div class="game-team-name away-team-name">{{ item.platforms.pinnacle.teamAwayName }}</div>
+            </div>
+            <a-button class="remove-button" type="link" @click="removeGamesRelation(item)">
+              <template #icon>
+                <link-outlined />
+                <disconnect-outlined />
+              </template>
+            </a-button>
+            <div class="game-date-time">{{ dayjs(item.timestamp).format('MM-DD HH:mm') }}</div>
+          </a-list-item>
+        </template>
+      </a-list>
+    </div>
+  </div>
+
+</template>
+
+<style lang="scss" scoped>
+.games-container {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: stretch;
+  height: calc(100vh - 126px);
+  padding: 15px;
+  gap: 15px;
+  border-top: 1px solid rgba(5, 5, 5, 0.06);
+}
+.games-column {
+  overflow: auto;
+  flex: 1;
+  h3 {
+    height: 39px;
+    margin: 0;
+    line-height: 39px;
+    text-align: center;
+    font-size: 14px;
+    font-weight: 600;
+    border: 1px solid rgba(5, 5, 5, 0.06);
+    border-bottom: none;
+    border-radius: 8px 8px 0 0;
+    background-color: #fafafa;
+  }
+}
+.games-column.relations {
+  flex: 2.5;
+  border-right: 0 none;
+}
+.ant-list {
+  max-height: calc(100vh - 196px);
+  overflow: auto;
+  border: 1px solid rgba(5, 5, 5, 0.06);
+  border-radius: 0 0 8px 8px;
+}
+.ant-list-item {
+  &:hover {
+    background-color: #fafafa;
+    .remove-button {
+      visibility: visible;
+    }
+  }
+  &.selected {
+    background-color: #e6f7ff;
+  }
+  &.disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
+  .remove-button {
+    visibility: hidden;
+    .anticon-disconnect {
+      display: none;
+    }
+    &:hover {
+      color: #f5222d;
+      .anticon-link {
+        display: none;
+      }
+      .anticon-disconnect {
+        margin-inline-start: 0;
+        display: inline-block;
+      }
+    }
+  }
+}
+.game-info {
+  flex: 1;
+  em {
+    font-style: normal;
+    font-size: 12px;
+  }
+}
+.game-league-name {
+  font-size: 16px;
+}
+.game-team-name {
+  color: #666;
+  &.home-team-name {
+    color: rgb(0, 107, 230);
+  }
+  &.away-team-name {
+    color: rgb(255, 56, 96);
+  }
+}
+.game-date-time {
+  color: #666;
+}
+</style>

+ 171 - 0
web/src/views/home.vue

@@ -0,0 +1,171 @@
+<script setup>
+import axios from 'axios';
+import dayjs from 'dayjs';
+import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue';
+import { message } from 'ant-design-vue';
+import { ReloadOutlined } from '@ant-design/icons-vue';
+
+const search = ref('');
+const games = ref(null);
+const refreshTimer = ref(null);
+
+const onSearch = (value) => {
+  search.value = value;
+};
+
+const updateSolutions = () => {
+  axios.get('/api/games/get_solutions', { params: { min_profit_rate: -1 } }).then(res => {
+    if (res.data.statusCode === 200) {
+      games.value = res.data.data;
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  });
+}
+
+const getSolutionIorsInfo = (sid) => {
+  axios.get('/api/games/get_solution_ior_info', { params: { sid } })
+  .then(res => {
+    if (res.data.statusCode === 200) {
+      console.log(res.data.data);
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  });
+}
+
+const betSolution = (sid, stake=0) => {
+  console.log('betSolution', sid, stake);
+  axios.get('/api/games/bet_solution', { params: { sid, stake } })
+  .then(res => {
+    if (res.data.statusCode === 200) {
+      console.log('betSolution result', res.data.data);
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+  })
+  .catch(err => {
+    const errorMessage = err.response?.data?.message ?? err.message;
+    message.error(errorMessage);
+    console.error('betSolution error', errorMessage, err);
+  });
+}
+
+const solutionsFiltered = computed(() => {
+  const searchValue = search.value.trim().toLowerCase();
+  return games.value?.filter(item => {
+    const { platforms: { polymarket, pinnacle }, timestamp } = item;
+    const dateTime = dayjs(timestamp).format('MM-DD HH:mm');
+    item.dateTime = dateTime;
+    return !searchValue
+    || polymarket.leagueName?.toLowerCase().includes(searchValue)
+    || polymarket.teamHomeName?.toLowerCase().includes(searchValue)
+    || polymarket.teamAwayName?.toLowerCase().includes(searchValue)
+    || pinnacle.leagueName?.toLowerCase().includes(searchValue)
+    || pinnacle.teamHomeName?.toLowerCase().includes(searchValue)
+    || pinnacle.teamAwayName?.toLowerCase().includes(searchValue);
+  });
+});
+
+const refresh = () => {
+  updateSolutions();
+}
+
+onMounted(() => {
+  refresh();
+  refreshTimer.value = setInterval(refresh, 1000 * 1);
+});
+
+onUnmounted(() => {
+  clearInterval(refreshTimer.value);
+});
+
+</script>
+
+<template>
+  <a-page-header title="策略">
+    <template #extra>
+      <a-input-search
+        v-model:value="search"
+        placeholder="搜索"
+        :allowClear="true"
+        @search="onSearch"
+      />
+      <a-button @click="refresh">
+        <template #icon>
+          <reload-outlined />
+        </template>
+        刷新
+      </a-button>
+    </template>
+  </a-page-header>
+
+  <div class="solutions-container">
+    <a-list :data-source="solutionsFiltered" size="small">
+      <template #renderItem="{ item }">
+        <a-list-item>
+          <div class="game-info">
+            <div class="game-league-name">{{ item.platforms.pinnacle.leagueName }} [{{ item.platforms.polymarket.leagueName }}]</div>
+            <div class="game-team-name home-team-name">{{ item.platforms.pinnacle.teamHomeName }} [{{ item.platforms.polymarket.teamHomeName }}]</div>
+            <div class="game-team-name away-team-name">{{ item.platforms.pinnacle.teamAwayName }} [{{ item.platforms.polymarket.teamAwayName }}]</div>
+            <div class="game-date-time">{{ item.dateTime }}</div>
+          </div>
+          <div class="solutions-list">
+            <a-button v-for="solution in item.solutions" @click="betSolution(solution.sid)">{{ solution.sol.win_profit_rate }}</a-button>
+          </div>
+        </a-list-item>
+      </template>
+    </a-list>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.home-page {
+  padding: 30vh 15px 15px;
+  h1 {
+    font-size: 32px;
+    font-weight: normal;
+    text-align: center;
+    color: #999;
+  }
+}
+.solutions-container {
+  // display: flex;
+  // flex-direction: row;
+  // justify-content: space-between;
+  // align-items: stretch;
+  height: calc(100vh - 126px);
+  padding: 15px;
+  // gap: 15px;
+  border-top: 1px solid rgba(5, 5, 5, 0.06);
+}
+.solutions-list {
+  button {
+    &:not(:first-child) {
+      margin-left: 10px;
+    }
+  }
+}
+.game-team-name {
+  &.home-team-name {
+    color: rgb(0, 107, 230);
+  }
+  &.away-team-name {
+    color: rgb(255, 56, 96);
+  }
+}
+.game-date-time {
+  color: #666;
+}
+</style>

+ 314 - 0
web/src/views/leagues.vue

@@ -0,0 +1,314 @@
+<script setup>
+import axios from 'axios';
+import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue';
+import { message } from 'ant-design-vue';
+import { ReloadOutlined, PlusOutlined, LinkOutlined, DisconnectOutlined } from '@ant-design/icons-vue';
+
+const search = ref('');
+const leagues = ref(null);
+const leaguesRelations = ref([]);
+const selectedLeagues = reactive({
+  polymarket: null,
+  pinnacle: null,
+});
+
+const onSearch = (value) => {
+  search.value = value;
+};
+
+const updateLeagues = () => {
+  axios.get('/api/games/get_leagues').then(res => {
+    if (res.data.statusCode === 200) {
+      leagues.value = res.data.data;
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  });
+};
+
+const selectLeague = (type, league) => {
+  const { id, name } = league;
+  const currentLeagues = { id, name };
+  if (type === 'polymarket') {
+    if (selectedLeagues.polymarket?.id === id) {
+      selectedLeagues.polymarket = null;
+    }
+    else {
+      selectedLeagues.polymarket = currentLeagues;
+    }
+  }
+  else if (type === 'pinnacle') {
+    if (selectedLeagues.pinnacle?.id === id) {
+      selectedLeagues.pinnacle = null;
+    }
+    else {
+      selectedLeagues.pinnacle = currentLeagues;
+    }
+  }
+  else {
+    throw new Error('invalid type');
+  }
+}
+
+const resetSelectedLeagues = () => {
+  selectedLeagues.polymarket = null;
+  selectedLeagues.pinnacle = null;
+}
+
+const setLeaguesRelation = () => {
+  if (!selectedLeagues.polymarket || !selectedLeagues.pinnacle) {
+    message.error('请选择要添加的联赛');
+    return;
+  }
+  const leagueRelation = {
+    id: selectedLeagues.polymarket.id,
+    platforms: selectedLeagues,
+  };
+  axios.post('/api/games/set_leagues_relation', leagueRelation)
+  .then(res => {
+    if (res.data.statusCode === 200) {
+      message.success('添加成功');
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+    refresh();
+    resetSelectedLeagues();
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  });
+};
+
+const removeLeaguesRelation = (relation) => {
+  axios.post('/api/games/remove_leagues_relation', { id: relation.id })
+  .then(res => {
+    if (res.data.statusCode === 200) {
+      message.success('删除成功');
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+    refresh();
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  });
+}
+
+const getLeaguesRelations = () => {
+  axios.get('/api/games/get_leagues_relations')
+  .then(res => {
+    if (res.data.statusCode === 200) {
+      leaguesRelations.value = res.data.data;
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  });
+};
+
+const refresh = () => {
+  updateLeagues();
+  getLeaguesRelations();
+}
+
+const getLeagues = (platform) => {
+  const searchValue = search.value.trim().toLowerCase();
+  return leagues.value?.[platform].map(item => {
+    const { id } = item;
+    let selected = false;
+    let disabeld = false;
+    if (selectedLeagues[platform]?.id === id) {
+      selected = true;
+    }
+    if (leaguesRelations.value.some(relation => relation.platforms[platform]?.id === id)) {
+      disabeld = true;
+    }
+    return { ...item, selected, disabeld };
+  }).filter(item => {
+    const { name, localesName='', sport='' } = item;
+    return !searchValue || name.toLowerCase().includes(searchValue) || localesName.toLowerCase().includes(searchValue) || sport.toLowerCase().includes(searchValue);
+  }) ?? [];
+}
+
+const polymarketLeagues = computed(() => {
+  return getLeagues('polymarket');
+});
+
+const pinnacleLeagues = computed(() => {
+  return getLeagues('pinnacle');
+});
+
+const leaguesRelationsFiltered = computed(() => {
+  const searchValue = search.value.trim().toLowerCase();
+  return leaguesRelations.value.filter(item => {
+    const { platforms: { polymarket, pinnacle } } = item;
+    return !searchValue
+    || polymarket.localesName?.toLowerCase().includes(searchValue)
+    || polymarket.name?.toLowerCase().includes(searchValue)
+    || pinnacle.name?.toLowerCase().includes(searchValue);
+  }) ?? [];
+});
+
+onMounted(() => {
+  refresh();
+});
+</script>
+
+<template>
+  <a-page-header title="联赛">
+    <template #extra>
+      <a-input-search
+        v-model:value="search"
+        placeholder="搜索"
+        :allowClear="true"
+        @search="onSearch"
+      />
+      <a-button @click="refresh">
+        <template #icon>
+          <reload-outlined />
+        </template>
+        刷新
+      </a-button>
+      <a-button type="primary" @click="setLeaguesRelation">
+        <template #icon>
+          <plus-outlined />
+        </template>
+        添加
+      </a-button>
+    </template>
+  </a-page-header>
+
+  <div class="leagues-container">
+    <div class="leagues-column polymarket">
+      <h3>Polymarket 联赛 ({{ polymarketLeagues.length }})</h3>
+      <a-list item-layout="horizontal" :data-source="polymarketLeagues" size="small">
+        <template #renderItem="{ item }">
+          <a-list-item :class="{ selected: item.selected, disabled: item.disabeld }" @click="!item.disabeld && selectLeague('polymarket', item)">
+            {{ item.localesName }} <em>{{ item.name }} {{ item.sport ? `[${item.sport.toUpperCase()}]` : '' }}</em>
+          </a-list-item>
+        </template>
+      </a-list>
+    </div>
+    <div class="leagues-column pinnacle">
+      <h3>Pinnacle 联赛 ({{ pinnacleLeagues.length }})</h3>
+      <a-list item-layout="horizontal" :data-source="pinnacleLeagues" size="small">
+        <template #renderItem="{ item }">
+          <a-list-item :class="{ selected: item.selected, disabled: item.disabeld }" @click="!item.disabeld && selectLeague('pinnacle', item)">
+            {{ item.name }}
+          </a-list-item>
+        </template>
+      </a-list>
+    </div>
+    <div class="leagues-column relations">
+      <h3>已添加的联赛 ({{ leaguesRelationsFiltered.length }})</h3>
+      <a-list item-layout="horizontal" :data-source="leaguesRelationsFiltered" size="small">
+        <template #renderItem="{ item }">
+          <a-list-item>
+            <div class="league-name polymarket-name">{{ item.platforms.polymarket.localesName }} <em>{{ item.platforms.polymarket.name }}</em></div>
+            <a-button class="remove-button" type="link" @click="removeLeaguesRelation(item)">
+              <template #icon>
+                <link-outlined />
+                <disconnect-outlined />
+              </template>
+            </a-button>
+            <div class="league-name pinnacle-name">{{ item.platforms.pinnacle.name }}</div>
+          </a-list-item>
+        </template>
+      </a-list>
+    </div>
+  </div>
+
+</template>
+
+<style lang="scss" scoped>
+.leagues-container {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: stretch;
+  height: calc(100vh - 126px);
+  padding: 15px;
+  gap: 15px;
+  border-top: 1px solid rgba(5, 5, 5, 0.06);
+}
+.leagues-column {
+  overflow: auto;
+  flex: 1;
+  h3 {
+    height: 39px;
+    margin: 0;
+    line-height: 39px;
+    text-align: center;
+    font-size: 14px;
+    font-weight: 600;
+    border: 1px solid rgba(5, 5, 5, 0.06);
+    border-bottom: none;
+    border-radius: 8px 8px 0 0;
+    background-color: #fafafa;
+  }
+}
+.leagues-column.relations {
+  flex: 2.5;
+  border-right: 0 none;
+}
+.ant-list {
+  max-height: calc(100vh - 196px);
+  overflow: auto;
+  border: 1px solid rgba(5, 5, 5, 0.06);
+  border-radius: 0 0 8px 8px;
+}
+.ant-list-item {
+  &:hover {
+    background-color: #fafafa;
+    .remove-button {
+      visibility: visible;
+    }
+  }
+  &.selected {
+    background-color: #e6f7ff;
+  }
+  &.disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
+  .league-name {
+    flex: 1;
+  }
+  .pinnacle-name {
+    text-align: right;
+  }
+  .remove-button {
+    visibility: hidden;
+    .anticon-disconnect {
+      display: none;
+    }
+    &:hover {
+      color: #f5222d;
+      .anticon-link {
+        display: none;
+      }
+      .anticon-disconnect {
+        margin-inline-start: 0;
+        display: inline-block;
+      }
+    }
+  }
+  em {
+    font-style: normal;
+    font-size: 12px;
+  }
+}
+</style>

+ 133 - 0
web/src/views/locales.vue

@@ -0,0 +1,133 @@
+<script setup>
+import axios from 'axios';
+import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue';
+import { message } from 'ant-design-vue';
+import { ReloadOutlined, PlusOutlined } from '@ant-design/icons-vue';
+
+const search = ref('');
+const locales = ref(null);
+const setTimer = ref(null);
+const pageSize = ref(20);
+
+const onSearch = (value) => {
+  search.value = value;
+};
+
+const updateLocales = () => {
+  axios.get('/api/locales/get_locales').then(res => {
+    if (res.data.statusCode === 200) {
+      const data = res.data.data ?? {};
+      locales.value = Object.keys(data).map(key => {
+        const value = data[key];
+        return { en: key, zh: value };
+      });
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  });
+};
+
+const setLocales = (en, zh) => {
+  axios.post('/api/locales/set_locales', { [en]: zh }).then(res => {
+    if (res.data.statusCode === 200) {
+      message.success('设置成功');
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  });
+}
+
+const changeLocales = (record) => {
+  clearTimeout(setTimer.value);
+  const { en, zh } = record;
+  setTimer.value = setTimeout(() => {
+    setLocales(en, zh);
+  }, 2000);
+}
+
+const localesList = computed(() => {
+  const searchValue = search.value.trim().toLowerCase();
+  return locales.value?.filter(item => {
+    return !searchValue || item.en.toLowerCase().includes(searchValue) || item.zh.toLowerCase().includes(searchValue);
+  }) ?? [];
+});
+
+const tableChange = (pagination) => {
+  pageSize.value = pagination.pageSize;
+  updateLocales();
+};
+
+onMounted(() => {
+  updateLocales();
+});
+onUnmounted(() => {
+  clearTimeout(setTimer.value);
+});
+</script>
+
+<template>
+  <a-page-header title="翻译">
+    <template #extra>
+      <a-input-search
+        v-model:value="search"
+        placeholder="搜索"
+        :allowClear="true"
+        @search="onSearch"
+      />
+      <a-button @click="updateLocales">
+        <template #icon>
+          <reload-outlined />
+        </template>
+        刷新
+      </a-button>
+    </template>
+  </a-page-header>
+
+  <div class="locales-container">
+  <a-table
+    :data-source="localesList"
+    :columns="[
+      { title: 'English', dataIndex: 'en', key: 'en', editable: false },
+      { title: '中文', dataIndex: 'zh', key: 'zh', editable: true },
+    ]"
+    :pagination="{ pageSize }"
+    row-key="en"
+    bordered
+    size="small"
+    @change="tableChange"
+  >
+    <template #bodyCell="{ column, record, index }">
+      <template v-if="column.editable">
+        <a-input
+          v-model:value="record[column.dataIndex]"
+          @input="changeLocales(record)"
+        />
+      </template>
+      <template v-else>
+        {{ record[column.dataIndex] }}
+      </template>
+    </template>
+  </a-table>
+  </div>
+
+</template>
+
+<style lang="scss" scoped>
+.locales-container {
+  height: calc(100vh - 126px);
+  padding: 15px;
+  gap: 15px;
+  border-top: 1px solid rgba(5, 5, 5, 0.06);
+}
+
+</style>

+ 26 - 0
web/vite.config.js

@@ -0,0 +1,26 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import vueDevTools from 'vite-plugin-vue-devtools'
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [
+    vue(),
+    vueDevTools(),
+  ],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url))
+    },
+  },
+  server: {
+    port: 9021,
+    open: true,
+    host: '0.0.0.0',
+    proxy: {
+      '^/api': 'http://127.0.0.1:9020',
+    }
+  },
+})

Някои файлове не бяха показани, защото твърде много файлове са промени