mooket

银河奶牛历史价格(包含强化物品)history(enhancement included) price for milkywayidle

// ==UserScript==
// @name         mooket
// @namespace    http://tampermonkey.net/
// @version      20250422.2.2
// @description  银河奶牛历史价格(包含强化物品)history(enhancement included) price for milkywayidle
// @author       IOMisaka
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @icon         https://www.milkywayidle.com/favicon.svg
// @grant        none
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js
// @run-at       document-start
// @license MIT
// ==/UserScript==

(function () {
  'use strict';
  let injectSpace = "mwi";//use window.mwi to access the injected object
  if (window[injectSpace]) return;//已经注入
  let mwi = {//供外部调用的接口
    version: "0.2.0",//版本号,未改动原有接口只更新最后一个版本号,更改了接口会更改次版本号,主版本暂时不更新,等稳定之后再考虑主版本号更新
    MWICoreInitialized: false,//是否初始化完成,完成会还会通过window发送一个自定义事件 MWICoreInitialized

    /*一些可以直接用的游戏数据,欢迎大家一起来整理
    game.state.levelExperienceTable //经验表
    game.state.skillingActionTypeBuffsDict },
    game.state.characterActions //[0]是当前正在执行的动作,其余是队列中的动作
    */
    game: null,//注入游戏对象,可以直接访问游戏中的大量数据和方法以及消息事件等
    lang: null,//语言翻译, 例如中文物品lang.zh.translation.itemNames['/items/coin']
    buffCalculator: null,//注入buff计算对象buffCalculator.mergeBuffs()合并buffs,计算加成效果等
    alchemyCalculator: null,//注入炼金计算对象


    /* marketJson兼容接口 */
    get marketJson() {
      return this.MWICoreInitialized && new Proxy(this.coreMarket, {
        get(coreMarket, prop) {
          if (prop === "market") {
            return new Proxy(coreMarket, {
              get(coreMarket, itemHridOrName) {
                return coreMarket.getItemPrice(itemHridOrName);
              }
            });
          }
          return null;
        }

      });
    },
    coreMarket: null,//coreMarket.marketData 格式{"/items/apple_yogurt:0":{ask,bid,time}}
    itemNameToHridDict: null,//物品名称反查表
    ensureItemHrid: function (itemHridOrName) {
      let itemHrid = this.itemNameToHridDict[itemHridOrName];
      if (itemHrid) return itemHrid;
      if (itemHridOrName?.startsWith("/items/") && this?.game?.state?.itemDetailDict) return itemHridOrName;
      return null;
    },//各种名字转itemHrid,找不到返回原itemHrid或者null
    hookCallback: hookCallback,//hook回调,用于hook游戏事件等 例如聊天消息mwi.hookCallback(mwi.game, "handleMessageChatMessageReceived", (_,obj)=>{console.log(obj)})
    fetchWithTimeout: fetchWithTimeout,//带超时的fetch
  };
  window[injectSpace] = mwi;

  async function patchScript(node) {
    try {
      const scriptUrl = node.src;
      node.remove();
      const response = await fetch(scriptUrl);
      if (!response.ok) throw new Error(`Failed to fetch script: ${response.status}`);

      let sourceCode = await response.text();

      // Define injection points as configurable patterns
      const injectionPoints = [
        {
          pattern: "Ca.a.use",
          replacement: `window.${injectSpace}.lang=Oa;Ca.a.use`,
          description: "注入语言翻译对象"
        },
        {
          pattern: "class lp extends s.a.Component{constructor(e){var t;super(e),t=this,",
          replacement: `class lp extends s.a.Component{constructor(e){var t;super(e),t=this,window.${injectSpace}.game=this,`,
          description: "注入游戏对象"

        },
        {
          pattern: "var Q=W;",
          replacement: `window.${injectSpace}.buffCalculator=W;var Q=W;`,
          description: "注入buff计算对象"
        },
        {
          pattern: "class Dn",
          replacement: `window.${injectSpace}.alchemyCalculator=Mn;class Dn`,
          description: "注入炼金计算对象"
        },
        {
          pattern: "var z=q;",
          replacement: `window.${injectSpace}.actionManager=q;var z=q;`,
          description: "注入动作管理对象"
        }
      ];

      injectionPoints.forEach(({ pattern, replacement, description }) => {
        if (sourceCode.includes(pattern)) {
          sourceCode = sourceCode.replace(pattern, replacement);
          console.info(`MWICore injecting: ${description}`);
        } else {
          console.warn(`MWICore injecting failed: ${description}`);
        }
      });

      const newNode = document.createElement('script');
      newNode.textContent = sourceCode;
      document.body.appendChild(newNode);
      console.info('MWICore patched successfully.')
    } catch (error) {
      console.error('MWICore patching failed:', error);
    }
  }
  new MutationObserver((mutationsList, obs) => {
    mutationsList.forEach((mutationRecord) => {
      for (const node of mutationRecord.addedNodes) {
        if (node.src) {
          if (node.src.search(/.*main\..*\.chunk.js/) === 0) {
            obs.disconnect();
            patchScript(node);
          }
        }
      }
    });
  }).observe(document, { childList: true, subtree: true });

  /**
   * Hook回调函数并添加后处理
   * @param {Object} targetObj 目标对象
   * @param {string} callbackProp 回调属性名
   * @param {Function} handler 后处理函数
   */
  function hookCallback(targetObj, callbackProp, handler) {
    const originalCallback = targetObj[callbackProp];

    if (!originalCallback) {
      throw new Error(`Callback ${callbackProp} does not exist`);
    }

    targetObj[callbackProp] = function (...args) {
      const result = originalCallback.apply(this, args);

      // 异步处理
      if (result && typeof result.then === 'function') {
        return result.then(res => {
          handler(res, ...args);
          return res;
        });
      }

      // 同步处理
      handler(result, ...args);
      return result;
    };

    // 返回取消Hook的方法
    return () => {
      targetObj[callbackProp] = originalCallback;
    };
  }
  /**
   * 带超时功能的fetch封装
   * @param {string} url - 请求URL
   * @param {object} options - fetch选项
   * @param {number} timeout - 超时时间(毫秒),默认10秒
   * @returns {Promise} - 返回fetch的Promise
   */
  function fetchWithTimeout(url, options = {}, timeout = 10000) {
    // 创建AbortController实例
    const controller = new AbortController();
    const { signal } = controller;

    // 设置超时计时器
    const timeoutId = setTimeout(() => {
      controller.abort(new Error(`请求超时: ${timeout}ms`));
    }, timeout);

    // 合并选项,添加signal
    const fetchOptions = {
      ...options,
      signal
    };

    // 发起fetch请求
    return fetch(url, fetchOptions)
      .then(response => {
        // 清除超时计时器
        clearTimeout(timeoutId);

        if (!response.ok) {
          throw new Error(`HTTP错误! 状态码: ${response.status}`);
        }
        return response;
      })
      .catch(error => {
        // 清除超时计时器
        clearTimeout(timeoutId);

        // 如果是中止错误,重新抛出超时错误
        if (error.name === 'AbortError') {
          throw new Error(`请求超时: ${timeout}ms`);
        }
        throw error;
      });
  }
  class ReconnectWebSocket {
    constructor(url, options = {}) {
      this.url = url; // WebSocket 服务器地址
      this.reconnectInterval = options.reconnectInterval || 10000; // 重连间隔(默认 5 秒)
      this.heartbeatInterval = options.heartbeatInterval || 60000; // 心跳间隔(默认 60 秒)
      this.maxReconnectAttempts = options.maxReconnectAttempts || 9999999; // 最大重连次数
      this.reconnectAttempts = 0; // 当前重连次数
      this.ws = null; // WebSocket 实例
      this.heartbeatTimer = null; // 心跳定时器
      this.isManualClose = false; // 是否手动关闭连接

      // 绑定事件处理器
      this.onOpen = options.onOpen || (() => { });
      this.onMessage = options.onMessage || (() => { });
      this.onClose = options.onClose || (() => { });
      this.onError = options.onError || (() => { });

      this.connect();
    }

    // 连接 WebSocket
    connect() {
      this.ws = new WebSocket(this.url);

      // WebSocket 打开事件
      this.ws.onopen = () => {
        console.log('WebMooket connected');
        this.reconnectAttempts = 0; // 重置重连次数
        this.startHeartbeat(); // 启动心跳
        this.onOpen();
      };

      // WebSocket 消息事件
      this.ws.onmessage = (event) => {
        this.onMessage(event.data);
      };

      // WebSocket 关闭事件
      this.ws.onclose = () => {
        console.log('WebMooket disconnected');
        this.stopHeartbeat(); // 停止心跳
        this.onClose();

        if (!this.isManualClose) {
          this.reconnect();
        }
      };

      // WebSocket 错误事件
      this.ws.onerror = (error) => {
        console.error('WebMooket error:', error);
        this.onError(error);
      };
    }

    // 启动心跳
    startHeartbeat() {
      this.heartbeatTimer = setInterval(() => {
        if (this.ws.readyState === WebSocket.OPEN) {
          //this.ws.send("ping");
        }
      }, this.heartbeatInterval);
    }

    // 停止心跳
    stopHeartbeat() {
      if (this.heartbeatTimer) {
        clearInterval(this.heartbeatTimer);
        this.heartbeatTimer = null;
      }
    }

    // 自动重连
    reconnect() {
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        console.log(`Reconnecting in ${this.reconnectInterval / 1000} seconds...`);
        setTimeout(() => {
          this.reconnectAttempts++;
          this.connect();
        }, this.reconnectInterval);
      } else {
        console.error('Max reconnection attempts reached');
      }
    }

    // 发送消息
    send(data) {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send(data);
      } else {
        console.error('WebMooket is not open');
      }
    }

    // 手动关闭连接
    close() {
      this.isManualClose = true;
      this.ws.close();
    }
  }
  /*实时市场模块*/
  const HOST = "https://mooket.qi-e.top";
  const MWIAPI_URL = "https://raw.githubusercontent.com/holychikenz/MWIApi/main/milkyapi.json";

  class CoreMarket {
    marketData = {};//市场数据,带强化等级,存储格式{"/items/apple_yogurt:0":{ask,bid,time}}
    fetchTimeDict = {};//记录上次API请求时间,防止频繁请求
    ttl = 300;//缓存时间,单位秒
    trade_ws = null;
    constructor() {
      //core data
      let marketDataStr = localStorage.getItem("MWICore_marketData") || "{}";
      this.marketData = JSON.parse(marketDataStr);

      if (mwi.game?.state?.character?.gameMode === "standard") {//标准模式才连接ws服务器,铁牛模式不连接ws服务器
        this.trade_ws = new ReconnectWebSocket(`${HOST}/market/ws`);
      }

      //mwiapi data
      let mwiapiJsonStr = localStorage.getItem("MWIAPI_JSON") || localStorage.getItem("MWITools_marketAPI_json");
      let mwiapiObj = null;
      if (mwiapiJsonStr) {
        mwiapiObj = JSON.parse(mwiapiJsonStr);
        this.mergeMWIData(mwiapiObj);
      }
      if (!mwiapiObj || Date.now() / 1000 - mwiapiObj.time > 600) {//超过10分才更新
        fetch(MWIAPI_URL).then(res => {
          res.text().then(mwiapiJsonStr => {
            mwiapiObj = JSON.parse(mwiapiJsonStr);
            this.mergeMWIData(mwiapiObj);
            //更新本地缓存数据
            localStorage.setItem("MWIAPI_JSON", mwiapiJsonStr);//更新本地缓存数据
            console.info("MWIAPI_JSON updated:", new Date(mwiapiObj.time * 1000).toLocaleString());
          })
        });
      }
      (this.trade_ws ?? {}).onMessage = (data) => {
        if (data === "ping") { return; }//心跳包,忽略
        let obj = JSON.parse(data);
        if (obj && obj.type === "market_item_order_books_updated") {
          this.handleMessageMarketItemOrderBooksUpdated(obj, false);//收到市场服务器数据,不上传
        } else if (obj && obj.type === "ItemPrice") {
          this.processItemPrice(obj);
        } else {
          console.log(data);
        }


      }
      //市场数据更新
      hookCallback(mwi.game, "handleMessageMarketItemOrderBooksUpdated", (res, obj) => this.handleMessageMarketItemOrderBooksUpdated(obj, true));
      setInterval(() => { this.save(); }, 1000 * 600);//十分钟保存一次
    }
    handleMessageMarketItemOrderBooksUpdated(obj, upload = false) {
      //更新本地,游戏数据不带时间戳,市场服务器数据带时间戳
      let timestamp = obj.time || parseInt(Date.now() / 1000);
      let itemHrid = obj.marketItemOrderBooks.itemHrid;
      obj.marketItemOrderBooks?.orderBooks?.forEach((item, enhancementLevel) => {
        let bid = item.bids?.length > 0 ? item.bids[0].price : -1;
        let ask = item.asks?.length > 0 ? item.asks[0].price : -1;
        this.updateItem(itemHrid + ":" + enhancementLevel, { bid: bid, ask: ask, time: timestamp });
      });
      obj.time = timestamp;//添加时间戳
      //上报数据
      if (this.trade_ws) {//标准模式走ws
        if (!upload) return;//只在game收到消息的时候上报
        this.trade_ws.send(JSON.stringify(obj));//ws上报
      } else {//铁牛上报
        fetchWithTimeout(`${HOST}/market/upload/order`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json"
          },
          body: JSON.stringify(obj)
        });
      }
    }
    /**
     * 合并MWIAPI数据,只包含0级物品
     *
     * @param obj 包含市场数据的对象
     */
    mergeMWIData(obj) {
      Object.entries(obj.market).forEach(([itemName, price]) => {
        let itemHrid = mwi.ensureItemHrid(itemName);
        if (itemHrid) this.updateItem(itemHrid + ":" + 0, { bid: price.bid, ask: price.ask, time: obj.time }, false);//本地更新
      });
      this.save();
    }
    mergeCoreDataBeforeSave() {
      let obj = JSON.parse(localStorage.getItem("MWICore_marketData") || "{}");
      Object.entries(obj).forEach(([itemHridLevel, priceObj]) => {
        this.updateItem(itemHridLevel, priceObj, false);//本地更新
      });
      //不保存,只合并
    }
    save() {//保存到localStorage
      this.mergeCoreDataBeforeSave();//从其他角色合并保存的数据
      localStorage.setItem("MWICore_marketData", JSON.stringify(this.marketData));
    }

    /**
     * 部分特殊物品的价格
     * 例如金币固定1,牛铃固定为牛铃袋/10的价格
     * @param {string} itemHrid - 物品hrid
     * @returns {Price|null} - 返回对应商品的价格对象,如果没有则null
     */
    getSpecialPrice(itemHrid) {
      switch (itemHrid) {
        case "/items/coin":
          return { bid: 1, ask: 1, time: Date.now() / 1000 };
        case "/items/cowbell": {
          let cowbells = this.getItemPrice("/items/bag_of_10_cowbells");
          return cowbells && { bid: cowbells.bid / 10, ask: cowbells.ask / 10, time: cowbells.time };
        }
        default:
          return null;
      }
    }
    /**
     * 获取商品的价格
     *
     * @param {string} itemHridOrName 商品HRID或名称
     * @param {number} [enhancementLevel=0] 装备强化等级,普通商品默认为0
     * @param {boolean} [peek=false] 是否只查看本地数据,不请求服务器数据
     * @returns {number|null} 返回商品的价格,如果商品不存在或无法获取价格则返回null
     */
    getItemPrice(itemHridOrName, enhancementLevel = 0, peek = false) {
      let itemHrid = mwi.ensureItemHrid(itemHridOrName);
      if (!itemHrid) return null;
      let specialPrice = this.getSpecialPrice(itemHrid);
      if (specialPrice) return specialPrice;
      let itemHridLevel = itemHrid + ":" + enhancementLevel;

      let priceObj = this.marketData[itemHridLevel];
      if (peek) return priceObj;

      if (Date.now() / 1000 - this.fetchTimeDict[itemHridLevel] < this.ttl) return priceObj;//1分钟内直接返回本地数据,防止频繁请求服务器
      this.fetchTimeDict[itemHridLevel] = Date.now() / 1000;
      this.trade_ws?.send(JSON.stringify({ type: "GetItemPrice", name: itemHrid, level: enhancementLevel }));
      return priceObj;
    }
    processItemPrice(resObj) {
      let itemHridLevel = resObj.name + ":" + resObj.level;
      let priceObj = { bid: resObj.bid, ask: resObj.ask, time: resObj.time };
      if (resObj.ttl) this.ttl = resObj.ttl;//更新ttl
      this.updateItem(itemHridLevel, priceObj);
    }
    updateItem(itemHridLevel, priceObj, isFetch = true) {
      let localItem = this.marketData[itemHridLevel];
      if (isFetch) this.fetchTimeDict[itemHridLevel] = Date.now() / 1000;//fetch时间戳
      if (!localItem || localItem.time < priceObj.time) {//服务器数据更新则更新本地数据

        let risePercent = 0;
        if (localItem) {
          let oriPrice = (localItem.ask + localItem.bid);
          let newPrice = (priceObj.ask + priceObj.bid);
          if (oriPrice != 0) risePercent = newPrice / oriPrice - 1;
        }
        this.marketData[itemHridLevel] = { rise: risePercent, ask: priceObj.ask, bid: priceObj.bid, time: priceObj.time };//更新本地数据
        dispatchEvent(new CustomEvent("MWICoreItemPriceUpdated"), priceObj);//触发事件
      }
    }
    resetRise() {
      Object.entries(this.marketData).forEach(([k, v]) => {
        v.rise = 0;
      });
    }
    save() {
      localStorage.setItem("MWICore_marketData", JSON.stringify(this.marketData));
    }
  }

  function init() {
    mwi.itemNameToHridDict = {};
    Object.entries(mwi.lang.en.translation.itemNames).forEach(([k, v]) => { mwi.itemNameToHridDict[v] = k });
    Object.entries(mwi.lang.zh.translation.itemNames).forEach(([k, v]) => { mwi.itemNameToHridDict[v] = k });
    mwi.coreMarket = new CoreMarket();

    mwi.MWICoreInitialized = true;
    window.dispatchEvent(new CustomEvent("MWICoreInitialized"))
    console.info("MWICoreInitialized event dispatched. window.mwi.MWICoreInitialized=true");
  }
  new Promise(resolve => {
    const interval = setInterval(() => {
      if (mwi.game && mwi.lang && mwi?.game?.state?.character?.gameMode) {//等待必须组件加载完毕后再初始化
        clearInterval(interval);
        resolve();
      }
    }, 200);
  }).then(() => {
    init();
    mooket();
  });

  function mooket() {

    window.mwi.hookCallback(window.mwi.game, "handleMessageMarketListingsUpdated", (_, obj) => {
      obj.endMarketListings.forEach(order => {
        if (order.filledQuantity == 0) return;//没有成交的订单不记录
        let key = order.itemHrid + "_" + order.enhancementLevel;

        let tradeItem = trade_history[key] || {}
        if (order.isSell) {
          tradeItem.sell = order.price;
        } else {
          tradeItem.buy = order.price;
        }
        trade_history[key] = tradeItem;
      });
      if (window.mwi?.game?.state?.character?.gameMode === "standard")//只记录标准模式的数据,因为铁牛不能交易
        localStorage.setItem("mooket_trade_history", JSON.stringify(trade_history));//保存挂单数据
    });

    let trade_history = JSON.parse(localStorage.getItem("mooket_trade_history") || "{}");

    let cur_day = 1;
    let curHridName = null;
    let curLevel = 0;
    let curShowItemName = null;
    let chartWidth = 500;
    let chartHeight = 280

    let configStr = localStorage.getItem("mooket_config");
    let config = configStr ? JSON.parse(configStr) : { "dayIndex": 0, "visible": true, "filter": { "bid": true, "ask": true, "mean": true } };
    cur_day = config.day;//读取设置

    window.onresize = function () {
      checkSize();
    };
    function checkSize() {
      if (window.innerWidth < window.innerHeight) {//竖屏,强制设置
        config.w = chartWidth = window.innerWidth * 0.8;
        config.h = chartHeight = chartWidth * 0.6;
      } else {
        chartWidth = 400;
        chartHeight = 250;
      }
    }
    checkSize();

    // 创建容器元素并设置样式和位置
    const container = document.createElement('div');
    container.style.border = "1px solid #ccc"; //边框样式
    container.style.backgroundColor = config.bgcolor||"#000";
    container.style.position = "fixed";
    container.style.zIndex = 10000;
    container.style.top = `${Math.max(0, Math.min(config.y || 0, window.innerHeight - 50))}px`; //距离顶部位置
    container.style.left = `${Math.max(0, Math.min(config.x || 0, window.innerWidth - 50))}px`; //距离左侧位置
    container.style.width = `${Math.max(0, Math.min(config.w || chartWidth, window.innerWidth))}px`; //容器宽度
    container.style.height = `${Math.max(0, Math.min(config.h || chartHeight, window.innerHeight))}px`; //容器高度
    container.style.resize = "both";
    container.style.overflow = "auto";
    container.style.display = "none";
    container.style.flexDirection = "column";
    container.style.flex = "1";
    container.style.minHeight = "33px";
    container.style.minWidth = "68px";
    container.style.cursor = "move";
    container.style.userSelect = "none";

    let mouseDragging = false;
    let touchDragging = false;
    let offsetX, offsetY;

    let resizeEndTimer = null;
    container.addEventListener("resize", () => {
      if (resizeEndTimer) clearTimeout(resizeEndTimer);
      resizeEndTimer = setTimeout(save_config, 1000);
    });
    container.addEventListener("mousedown", function (e) {
      if (mouseDragging || touchDragging) return;
      const rect = container.getBoundingClientRect();
      if (container.style.resize === "both" && (e.clientX > rect.right - 10 || e.clientY > rect.bottom - 10)) return;
      mouseDragging = true;
      offsetX = e.clientX - container.offsetLeft;
      offsetY = e.clientY - container.offsetTop;
    });

    document.addEventListener("mousemove", function (e) {
      if (mouseDragging) {
        var newX = e.clientX - offsetX;
        var newY = e.clientY - offsetY;

        if (newX < 0) newX = 0;
        if (newY < 0) newY = 0;
        if (newX > window.innerWidth - container.offsetWidth) newX = window.innerWidth - container.offsetWidth;
        if (newY > window.innerHeight - container.offsetHeight) newY = window.innerHeight - container.offsetHeight;

        container.style.left = newX + "px";
        container.style.top = newY + "px";
      }
    });

    document.addEventListener("mouseup", function () {
      if (mouseDragging) {
        mouseDragging = false;
        save_config();
      }
    });

    container.addEventListener("touchstart", function (e) {
      if (mouseDragging || touchDragging) return;
      const rect = container.getBoundingClientRect();
      let touch = e.touches[0];
      if (container.style.resize === "both" && (e.clientX > rect.right - 10 || e.clientY > rect.bottom - 10)) return;
      touchDragging = true;
      offsetX = touch.clientX - container.offsetLeft;
      offsetY = touch.clientY - container.offsetTop;
    });

    document.addEventListener("touchmove", function (e) {
      if (touchDragging) {
        let touch = e.touches[0];
        var newX = touch.clientX - offsetX;
        var newY = touch.clientY - offsetY;

        if (newX < 0) newX = 0;
        if (newY < 0) newY = 0;
        if (newX > window.innerWidth - container.offsetWidth) newX = window.innerWidth - container.offsetWidth;
        if (newY > window.innerHeight - container.offsetHeight) newY = window.innerHeight - container.offsetHeight;

        container.style.left = newX + "px";
        container.style.top = newY + "px";
      }
    });

    document.addEventListener("touchend", function () {
      if (touchDragging) {
        touchDragging = false;
        save_config();
      }
    });
    document.body.appendChild(container);

    const ctx = document.createElement('canvas');
    ctx.id = "myChart";
    container.appendChild(ctx);



    // 创建下拉菜单并设置样式和位置
    let wrapper = document.createElement('div');
    wrapper.style.position = 'absolute';
    wrapper.style.top = '5px';
    wrapper.style.right = '16px';
    wrapper.style.fontSize = '14px';

    //wrapper.style.backgroundColor = '#fff';
    wrapper.style.flexShrink = 0;
    container.appendChild(wrapper);

    const days = [1, 3, 7, 14, 30, 180, 360];
    const dayTitle = ['1天', '3天', '1周', '2周', '1月', '半年', '一年'];
    cur_day = days[config.dayIndex];

    let select = document.createElement('select');
    select.style.cursor = 'pointer';
    select.style.verticalAlign = 'middle';
    select.onchange = function () {
      config.dayIndex = days.indexOf(parseInt(this.value));
      if (curHridName) requestItemPrice(curHridName, this.value, curLevel);
      save_config();
    };

    for (let i = 0; i < days.length; i++) {
      let option = document.createElement('option');
      option.value = days[i];
      option.text = dayTitle[i];
      if (i === config.dayIndex) option.selected = true;
      select.appendChild(option);
    }

    wrapper.appendChild(select);

    // 创建一个容器元素并设置样式和位置
    const leftContainer = document.createElement('div');
    leftContainer.style.padding = '2px'
    leftContainer.style.display = 'flex';
    leftContainer.style.flexDirection = 'row';
    leftContainer.style.alignItems = 'center'
    container.appendChild(leftContainer);

    //添加一个btn隐藏canvas和wrapper
    let btn_close = document.createElement('input');
    btn_close.type = 'button';
    btn_close.value = '📈隐藏';
    btn_close.style.margin = 0;
    btn_close.style.cursor = 'pointer';

    leftContainer.appendChild(btn_close);

    let picker = document.createElement('input');
    picker.type = 'color';
    picker.style.cursor = 'pointer';
    picker.style.width = '20px';
    picker.style.height = '20px';
    picker.style.padding = "0"
    picker.style.verticalAlign="middle"
    picker.onchange = function () {
      container.style.backgroundColor = this.value;
      config.bgcolor = this.value;
      save_config();
    }
    wrapper.appendChild(picker);



    //一个固定的文本显示买入卖出历史价格
    let price_info = document.createElement('div');

    price_info.style.fontSize = '14px';
    price_info.title = "我的最近买/卖价格"
    price_info.style.width = "max-content";
    price_info.style.whiteSpace = "nowrap";
    price_info.style.lineHeight = '25px';
    price_info.style.display = 'none';
    price_info.style.marginLeft = '5px';

    let buy_price = document.createElement('span');
    let sell_price = document.createElement('span');
    price_info.appendChild(buy_price);
    price_info.appendChild(sell_price);
    buy_price.style.color = 'red';
    sell_price.style.color = 'lime';

    leftContainer.appendChild(price_info);

    let lastWidth;
    let lastHeight;
    btn_close.onclick = toggle;
    function toggle() {
      if (wrapper.style.display === 'none') {
        wrapper.style.display = ctx.style.display = 'block';
        container.style.resize = "both";
        btn_close.value = '📈隐藏';
        leftContainer.style.position = 'absolute'
        leftContainer.style.top = '1px';
        leftContainer.style.left = '1px';
        container.style.width = lastWidth;
        container.style.height = lastHeight;
        config.visible = true;
        save_config();
      } else {
        lastWidth = container.style.width;
        lastHeight = container.style.height;
        wrapper.style.display = ctx.style.display = 'none';
        container.style.resize = "none";
        container.style.width = "auto";
        container.style.height = "auto";


        btn_close.value = '📈显示';
        leftContainer.style.position = 'relative'
        leftContainer.style.top = 0;
        leftContainer.style.left = 0;

        config.visible = false;
        save_config();
      }
    };

    let chart = new Chart(ctx, {
      type: 'line',
      data: {
        labels: [],
        datasets: [{
          label: '市场',
          data: [],
          backgroundColor: 'rgba(255,99,132,0.2)',
          borderColor: 'rgba(255,99,132,1)',
          borderWidth: 1
        }]
      },
      options: {
        onClick: save_config,
        responsive: true,
        maintainAspectRatio: false,
        pointRadius: 0,
        pointHitRadius: 20,
        scales: {
          y: {
            beginAtZero: false,
            ticks: {
              // 自定义刻度标签格式化
              callback: showNumber
            }
          }
        },
        plugins: {
          title: {
            display: true,
            text: "",
            color: "cornflowerblue",
            font: {
              size: 15,
              weight: 'bold',
            }
          }
        }
      }
    });

    function requestItemPrice(itemHridName, day = 1, level = 0) {
      if (!itemHridName) return;
      if (curHridName === itemHridName && curLevel === level && cur_day === day) return;//防止重复请求

      curHridName = itemHridName;
      curLevel = level;
      cur_day = day;

      curShowItemName = localStorage.getItem("i18nextLng")?.startsWith("zh") ?
        window.mwi.lang.zh.translation.itemNames[itemHridName] : window.mwi.lang.en.translation.itemNames[itemHridName];
      curShowItemName += curLevel > 0 ? "+" + curLevel : "";

      let time = day * 3600 * 24;
      //const HOST = "https://mooket.qi-e.top";上面定义了
      if (curLevel > 0 || day < 2) {
        const params = new URLSearchParams();
        params.append("name", curHridName);
        params.append("level", curLevel);
        params.append("time", time);
        fetch(`${HOST}/market/item/history?${params}`).then(res => {
          res.json().then(data => updateChart(data, cur_day));
        })
      }//新api
      else {//旧api
        let itemNameEN = window.mwi.game.state.itemDetailDict[itemHridName].name;
        fetch(`${HOST}/market`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            name: itemNameEN,
            time: time
          })
        }).then(res => {
          res.json().then(data => updateChart(data, cur_day));
        })
      }
    }

    function formatTime(timestamp, range) {
      const date = new Date(timestamp * 1000);
      const pad = n => n.toString().padStart(2, '0');

      // 获取各时间组件
      const hours = pad(date.getHours());
      const minutes = pad(date.getMinutes());
      const day = pad(date.getDate());
      const month = pad(date.getMonth() + 1);
      const shortYear = date.getFullYear().toString().slice(-2);

      // 根据时间范围选择格式
      switch (parseInt(range)) {
        case 1: // 1天:只显示时间
          return `${hours}:${minutes}`;

        case 3: // 3天:日+时段
          return `${hours}:${minutes}`;

        case 7: // 7天:月/日 + 时段
          return `${day}.${hours}`;
        case 14: // 14天:月/日 + 时段
          return `${day}.${hours}`;
        case 30: // 30天:月/日
          return `${month}/${day}`;

        default: // 180天:年/月
          return `${shortYear}/${month}`;
      }
    }

    function showNumber(num) {
      if (isNaN(num)) return num;
      if (num === 0) return "0"; // 单独处理0的情况

      const absNum = Math.abs(num);

      //num保留一位小数
      if (num < 1) return num.toFixed(2);

      return absNum >= 1e10 ? `${(num / 1e9).toFixed(1)}B` :
        absNum >= 1e7 ? `${(num / 1e6).toFixed(1)}M` :
          absNum >= 1e4 ? `${Math.floor(num / 1e3)}K` :
            `${Math.floor(num)}`;
    }
    //data={'bid':[{time:1,price:1}],'ask':[{time:1,price:1}]}
    function updateChart(data, day) {
      //字段名差异
      data.bid = data.bid || data.bids
      data.ask = data.ask || data.asks;
      //过滤异常元素
      for (let i = data.bid.length - 1; i >= 0; i--) {
        if (data.bid[i].price < 0 && data.ask[i].price < 0) {//都小于0,认为是异常数据,直接删除
          data.bid.splice(i, 1);
          data.ask.splice(i, 1);
        } else {//小于0则设置为0
          data.bid[i].price = Math.max(0, data.bid[i].price);
          data.ask[i].price = Math.max(0, data.ask[i].price);
        }
      }

      //timestamp转日期时间
      //根据day输出不同的时间表示,<3天显示时分,<=7天显示日时,<=30天显示月日,>30天显示年月

      //显示历史价格
      let enhancementLevel = document.querySelector(".MarketplacePanel_infoContainer__2mCnh .Item_enhancementLevel__19g-e")?.textContent.replace("+", "") || "0";
      let tradeName = curHridName + "_" + parseInt(enhancementLevel);
      if (trade_history[tradeName]) {
        let buy = trade_history[tradeName].buy || "无";
        let sell = trade_history[tradeName].sell || "无";
        price_info.style.display = "inline-block";
        let levelStr = enhancementLevel > 0 ? "(+" + enhancementLevel + ")" : "";
        price_info.innerHTML = `<span style="color:red">${showNumber(buy)}</span>/<span style="color:green">${showNumber(sell)}</span>${levelStr}`;
        container.style.minWidth = price_info.clientWidth + 70 + "px";

      } else {
        price_info.style.display = "none";
        container.style.minWidth = "68px";
      }

      let labels = data.bid.map(x => formatTime(x.time, day));

      chart.data.labels = labels;

      let sma = [];
      let sma_size = 6;
      let sma_window = [];
      for (let i = 0; i < data.bid.length; i++) {
        sma_window.push((data.bid[i].price + data.ask[i].price) / 2);
        if (sma_window.length > sma_size) sma_window.shift();
        sma.push(sma_window.reduce((a, b) => a + b, 0) / sma_window.length);
      }
      chart.options.plugins.title.text = curShowItemName
      chart.data.datasets = [
        {
          label: '买入',
          data: data.bid.map(x => x.price),
          borderColor: '#ff3300',
          backgroundColor: '#ff3300',
          borderWidth: 1.5
        },
        {
          label: '卖出',
          data: data.ask.map(x => x.price),
          borderColor: '#00cc00',
          backgroundColor: '#00cc00',
          borderWidth: 1.5
        },
        {
          label: '均线',
          data: sma,
          borderColor: '#ff9900',
          borderWidth: 3,
          tension: 0.5,
          fill: true
        }
      ];
      chart.setDatasetVisibility(0, config.filter.ask);
      chart.setDatasetVisibility(1, config.filter.bid);
      chart.setDatasetVisibility(2, config.filter.mean);

      chart.update()
    }
    function save_config() {

      if (chart && chart.data && chart.data.datasets && chart.data.datasets.length == 3) {
        config.filter.ask = chart.getDatasetMeta(0).visible;
        config.filter.bid = chart.getDatasetMeta(1).visible;
        config.filter.mean = chart.getDatasetMeta(2).visible;
      }
      config.x = Math.max(0, Math.min(container.getBoundingClientRect().x, window.innerWidth - 50));
      config.y = Math.max(0, Math.min(container.getBoundingClientRect().y, window.innerHeight - 50));
      if (container.style.width != "auto") {
        config.w = container.clientWidth;
        config.h = container.clientHeight;
      }

      localStorage.setItem("mooket_config", JSON.stringify(config));
    }
    setInterval(() => {
      if (document.querySelector(".MarketplacePanel_marketplacePanel__21b7o")?.checkVisibility()) {
        container.style.display = "block"
        try {
          let currentItem = document.querySelector(".MarketplacePanel_currentItem__3ercC");
          let level = currentItem?.querySelector(".Item_enhancementLevel__19g-e");
          let itemHrid = mwi.ensureItemHrid(currentItem?.querySelector(".Icon_icon__2LtL_")?.ariaLabel);
          requestItemPrice(itemHrid, cur_day, parseInt(level?.textContent.replace("+", "") || "0"))
        } catch (e) {
          console.log(e)
        }
      } else {
        container.style.display = "none"
      }
    }, 500);
    //setInterval(updateInventoryStatus, 60000);
    toggle();
    console.info("mooket 初始化完成");
  }
})();