NewBilibiliExp

B 站经验助手,自动投币视频、模拟移动端分享、经验获取统计、升级时间估计(2025年更新版)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name        NewBilibiliExp
// @namespace   NewBilibiliExp
// @match       *://www.bilibili.com/video/*
// @version     2.0.2
// @author      Dreace (原作者) & Updated by Mario
// @license     GPL-3.0
// @description B 站经验助手,自动投币视频、模拟移动端分享、经验获取统计、升级时间估计(2025年更新版)
// @grant       GM.xmlHttpRequest
// @grant       GM.setValue
// @grant       GM.getValue
// @grant       GM.deleteValue
// @grant       unsafeWindow
// @connect     api.bilibili.com
// @connect     www.bilibili.com
// @connect     at.alicdn.com
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require     https://cdn.jsdelivr.net/npm/[email protected]/js/md5.min.js
// ==/UserScript==

"use strict";

// 配置选项
const CONFIG = {
  AUTO_COIN: true,         // 是否自动投币
  AUTO_SHARE: true,        // 是否自动分享
  COIN_NUMBER: 1,          // 每个视频投币数量 (1 或 2)
  SHOW_STATS: true,        // 是否显示经验统计
  AUTO_VIP_EXP: true,      // 是否自动获取大会员观看经验
};

// API端点
const API = {
  NAV: "https://api.bilibili.com/x/web-interface/nav",
  REWARD: "https://api.bilibili.com/x/member/web/exp/reward",
  COIN_ADD: "https://api.bilibili.com/x/web-interface/coin/add",
  SHARE: "https://api.bilibili.com/x/web-interface/share/add",
  WBI_IMG: "https://api.bilibili.com/x/web-interface/nav",
  VIP_EXP: "https://api.bilibili.com/x/vip/experience/add",
};

// WBI签名相关
const WBI = {
  img_key: "",
  sub_key: "",
  
  // 初始化WBI密钥
  async init() {
    try {
      const res = await request({
        url: API.WBI_IMG,
        method: "GET"
      });
      
      if (res.code === 0 && res.data.wbi_img) {
        const { img_url, sub_url } = res.data.wbi_img;
        this.img_key = this.extractKey(img_url);
        this.sub_key = this.extractKey(sub_url);
        return true;
      }
      return false;
    } catch (err) {
      console.error("[BilibiliExp] WBI初始化失败", err);
      return false;
    }
  },
  
  // 从URL提取密钥
  extractKey(url) {
    return url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.'));
  },
  
  // 混合密钥
  getMixinKey() {
    if (!this.img_key || !this.sub_key) return '';
    
    const mixed_key = this.img_key + this.sub_key;
    const MIXIN_KEY_ORDER = [46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13];
    let result = '';
    
    for (let i = 0; i < MIXIN_KEY_ORDER.length; i++) {
      if (MIXIN_KEY_ORDER[i] < mixed_key.length) {
        result += mixed_key[MIXIN_KEY_ORDER[i]];
      }
    }
    
    return result;
  },
  
  // 对参数进行WBI签名
  sign(params) {
    if (!this.img_key || !this.sub_key) {
      console.error("[BilibiliExp] WBI密钥未初始化");
      return params;
    }
    
    // 添加时间戳和随机数
    params.wts = Math.floor(Date.now() / 1000);
    
    // 对参数排序
    const query = Object.keys(params)
      .sort()
      .map(key => `${key}=${params[key]}`)
      .join('&');
    
    // 计算w_rid
    const wrid = md5(query + this.getMixinKey());
    
    // 返回带签名的参数
    return {
      ...params,
      w_rid: wrid
    };
  }
};

// 用户信息模块
const UserInfo = {
  // 获取用户信息
  async get() {
    try {
      const res = await request({
        url: API.NAV,
        method: "GET"
      });
      
      if (res.code === 0) {
        return {
          isLogin: res.data.isLogin,
          money: res.data.money,
          uname: res.data.uname,
          level_info: res.data.level_info,
          isVip: res.data.vipStatus === 1 || res.data.vip_status === 1, // 检查是否为大会员
          vipType: res.data.vipType || res.data.vip_type || 0 // 大会员类型
        };
      }
      return null;
    } catch (err) {
      console.error("[BilibiliExp] 获取用户信息失败", err);
      return null;
    }
  }
};

// 每日奖励模块
const DailyReward = {
  // 获取每日奖励状态
  async getStatus() {
    try {
      const res = await request({
        url: API.REWARD,
        method: "GET"
      });
      
      if (res.code === 0) {
        // 如果数据不包含level_info,获取用户信息以补充
        if (!res.data.level_info) {
          console.log("[BilibiliExp] 奖励API返回数据不包含等级信息,尝试从用户信息获取");
          const userInfo = await UserInfo.get();
          if (userInfo && userInfo.level_info) {
            res.data.level_info = userInfo.level_info;
            // 添加大会员状态信息
            res.data.isVip = userInfo.isVip;
            res.data.vipType = userInfo.vipType;
          } else {
            // 尝试从页面元素获取
            res.data.level_info = this.getUserLevelInfo();
            // 页面中无法可靠获取大会员状态,设为false
            res.data.isVip = false;
          }
        }
        
        // 补充大会员观看经验获取状态
        res.data.vipExp = VIPExpModule.hasGotVipExp();
        
        return res.data;
      }
      return null;
    } catch (err) {
      console.error("[BilibiliExp] 获取每日奖励状态失败", err);
      return null;
    }
  },
  
  // 计算升级所需天数
  calculateDaysToUpgrade(rewardInfo) {
    // 增强防御性检查
    if (!rewardInfo || !rewardInfo.level_info) {
      console.warn("[BilibiliExp] 无法计算升级天数:缺少等级信息");
      return 0;
    }
    
    const { current_level, current_exp, next_exp } = rewardInfo.level_info;
    
    // 确保所有需要的字段都存在
    if (current_level === undefined || current_exp === undefined || next_exp === undefined) {
      console.warn("[BilibiliExp] 无法计算升级天数:等级信息不完整");
      return 0;
    }
    
    if (current_level >= 6) return 0;
    
    // 根据是否为大会员确定每日经验值
    const exp_per_day = rewardInfo.isVip ? 75 : 65;
    return Math.ceil((next_exp - current_exp) / exp_per_day);
  },
  
  // 从页面元素获取用户等级信息
  getUserLevelInfo() {
    // 尝试从页面元素获取等级信息
    try {
      // 尝试从顶部用户信息获取
      const levelElement = document.querySelector('.user-level') || 
                          document.querySelector('.level-info') ||
                          document.querySelector('[class*="level"]');
      
      if (levelElement) {
        const levelText = levelElement.textContent || '';
        const levelMatch = levelText.match(/Lv\.(\d+)/);
        if (levelMatch && levelMatch[1]) {
          const level = parseInt(levelMatch[1]);
          // 创建一个简单的等级信息对象
          return {
            current_level: level,
            current_exp: 0,
            next_exp: level >= 6 ? 0 : 1
          };
        }
      }
      
      // 备选:检查页面中是否有其他地方显示了等级
      const pageContent = document.body.textContent || '';
      const levelMatches = pageContent.match(/LV\.(\d+)/i) || pageContent.match(/等级[::]?\s*(\d+)/);
      if (levelMatches && levelMatches[1]) {
        const level = parseInt(levelMatches[1]);
        return {
          current_level: level,
          current_exp: 0,
          next_exp: level >= 6 ? 0 : 1
        };
      }
    } catch (err) {
      console.error("[BilibiliExp] 从页面获取等级信息失败", err);
    }
    
    // 如果无法从页面获取,返回默认值
    console.log("[BilibiliExp] 无法从页面获取等级信息,使用默认值");
    return {
      current_level: 0,
      current_exp: 0,
      next_exp: 1
    };
  }
};

// 投币模块
const CoinModule = {
  // 给视频投币
  async add(aid, num = 1) {
    if (!aid) return { code: -1, message: "视频ID为空" };
    
    const csrf = getCookie("bili_jct");
    if (!csrf) return { code: -1, message: "未获取到CSRF令牌" };
    
    // 准备参数并添加WBI签名
    const params = WBI.sign({
      aid: aid,
      multiply: num,
      select_like: 1,
      csrf: csrf
    });
    
    try {
      const res = await request({
        url: API.COIN_ADD,
        method: "POST",
        data: params
      });
      
      return res;
    } catch (err) {
      console.error("[BilibiliExp] 投币失败", err);
      return { code: -1, message: "投币请求失败" };
    }
  }
};

// 分享模块
const ShareModule = {
  // 分享视频
  async share(aid) {
    if (!aid) return { code: -1, message: "视频ID为空" };
    
    const csrf = getCookie("bili_jct");
    if (!csrf) return { code: -1, message: "未获取到CSRF令牌" };
    
    // 准备参数并添加WBI签名
    const params = WBI.sign({
      aid: aid,
      csrf: csrf
    });
    
    try {
      const res = await request({
        url: API.SHARE,
        method: "POST",
        data: params
      });
      
      return res;
    } catch (err) {
      console.error("[BilibiliExp] 分享失败", err);
      return { code: -1, message: "分享请求失败" };
    }
  }
};

// 大会员经验模块
const VIPExpModule = {
  // 本地存储键名
  STORAGE_KEY: 'bili_vip_exp_status',
  
  // 检查是否已获取大会员观看经验
  hasGotVipExp() {
    const status = this.getStoredStatus();
    if (!status) return false;
    
    // 检查日期是否为今天
    const today = new Date().toDateString();
    if (status.date !== today) {
      // 如果不是今天的记录,返回false
      return false;
    }
    
    return status.hasGotExp === true;
  },
  
  // 获取存储状态
  getStoredStatus() {
    try {
      const data = localStorage.getItem(this.STORAGE_KEY);
      if (data) {
        return JSON.parse(data);
      }
    } catch (err) {
      console.error("[BilibiliExp] 读取本地存储状态失败", err);
    }
    return null;
  },
  
  // 存储状态
  saveStatus(hasGotExp) {
    try {
      const status = {
        date: new Date().toDateString(),
        hasGotExp: hasGotExp
      };
      localStorage.setItem(this.STORAGE_KEY, JSON.stringify(status));
    } catch (err) {
      console.error("[BilibiliExp] 保存本地存储状态失败", err);
    }
  },
  
  // 获取大会员观看经验
  async getVipExp() {
    // 检查是否已经获取过
    if (this.hasGotVipExp()) {
      console.log("[BilibiliExp] 今日已获取过大会员观看经验");
      return { code: 0, message: "已经获取过", data: { is_grant: true } };
    }
    
    const csrf = getCookie("bili_jct");
    if (!csrf) {
      return { code: -1, message: "未获取到CSRF令牌" };
    }
    
    // 准备参数
    const params = {
      csrf: csrf
    };
    
    try {
      const res = await request({
        url: API.VIP_EXP,
        method: "POST",
        data: params,
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
        }
      });
      
      // //打印res字符串
      // console.log(res);

      //{code: 69198, message: '用户经验已经领取', ttl: 1, data: {is_grant:false, type: 0}}
      if (res.code === 69198) {
        this.saveStatus(true);
        console.log("[BilibiliExp] 用户已经领取过大会员经验");
      }

      // 如果获取成功,更新本地存储
      if (res.code === 0 && res.data && res.data.is_grant) {
        this.saveStatus(true);
        console.log("[BilibiliExp] 成功获取大会员观看经验");
      } else {
        console.log(`[BilibiliExp] 获取大会员观看经验失败: ${res.message || '未知错误'}`);
      }
      
      return res;
    } catch (err) {
      console.error("[BilibiliExp] 获取大会员观看经验失败", err);
      return { code: -1, message: "请求失败" };
    }
  }
};

// UI渲染模块
const UIRenderer = {
  // 插入经验统计界面
  async renderExpStats() {
    if (!CONFIG.SHOW_STATS) return;
    
    try {
      const rewardInfo = await DailyReward.getStatus();
      if (!rewardInfo) {
        console.error("[BilibiliExp] 无法获取奖励状态,取消统计显示");
        return false;
      }
      
      // 创建图标样式
      this.insertIconStyle();
      
      // 准备统计数据
      const statsData = this.prepareStatsData(rewardInfo);
      
      // 检查是否获取到了统计数据
      if (!statsData || statsData.length === 0) {
        console.error("[BilibiliExp] 统计数据为空,取消显示");
        return false;
      }
      
      // 尝试创建一个我们自己的容器,避免修改B站元素
      // 首先查找是否已经存在我们的容器
      let ourContainer = document.querySelector(".bili-exp-container");
      
      if (!ourContainer) {
        // 获取工具栏位置 - 尝试多个可能的选择器
        const toolbarSelectors = [
          ".video-toolbar-v1",            // 原始选择器
          ".video-toolbar-container",     // 2024年可能的新选择器
          ".bpx-player-video-toolbar",    // B站可能的播放器工具栏
          ".video-info-container",        // 视频信息容器
          ".video-info-operation",        // 操作按钮区域
          ".video-tool-bar",              // 可能的工具栏类名
          ".bili-video-toolbar",          // 另一个可能的工具栏类名
          ".operation-list",              // 操作列表
          "[data-type='toolbar']"         // 通过数据属性查找
        ];
        
        // 尝试查找工具栏
        let toolbar = null;
        for (const selector of toolbarSelectors) {
          toolbar = document.querySelector(selector);
          if (toolbar) {
            console.log(`[BilibiliExp] 找到工具栏元素: ${selector}`);
            
            // 使用辅助函数应用样式
            this.applyToolbarStyle(toolbar, selector);
            
            // 创建我们自己的容器,附加到工具栏
            ourContainer = document.createElement("div");
            ourContainer.className = "bili-exp-container";
            // 通过插入而不是appendChild,避免可能的DOM冲突
            try {
              toolbar.insertAdjacentElement('beforeend', ourContainer);
              console.log(`[BilibiliExp] 成功在${selector}中插入容器`);
            } catch (e) {
              console.error(`[BilibiliExp] 插入容器失败,尝试appendChild`, e);
              try {
                toolbar.appendChild(ourContainer);
              } catch (e2) {
                console.error(`[BilibiliExp] appendChild也失败`, e2);
                // 继续尝试其他容器
                ourContainer = null;
                continue;
              }
            }
            break;
          }
        }
        
        if (!ourContainer) {
          // 降级方案 - 尝试查找其他可能的容器
          const alternativeContainers = [
            ".video-container-v1",          // 视频容器
            ".bpx-player-container",        // 播放器容器
            "#bilibili-player",             // 播放器元素
            "#playerWrap",                  // 播放器包装
            ".player-wrap",                 // 播放器包装
            ".video-container"              // 通用视频容器
          ];
          
          for (const selector of alternativeContainers) {
            const container = document.querySelector(selector);
            if (container) {
              console.log(`[BilibiliExp] 找到替代容器: ${selector}`);
              // 创建我们自己的容器
              ourContainer = document.createElement("div");
              ourContainer.className = "bili-exp-container";
              ourContainer.style.cssText = "margin-top: 10px; padding: 8px; background: rgba(0,0,0,0.03); border-radius: 6px;";
              
              try {
                container.insertAdjacentElement('beforeend', ourContainer);
              } catch (e) {
                console.error(`[BilibiliExp] 插入容器失败,尝试appendChild`, e);
                try {
                  container.appendChild(ourContainer);
                } catch (e2) {
                  console.error(`[BilibiliExp] appendChild也失败`, e2);
                  // 继续尝试其他容器
                  ourContainer = null;
                  continue;
                }
              }
              break;
            }
          }
        }
      }
      
      if (!ourContainer) {
        // 最终降级方案 - 直接添加到页面底部
        console.log("[BilibiliExp] 未找到任何合适的容器,将添加到页面底部");
        ourContainer = document.createElement("div");
        ourContainer.className = "bili-exp-container bili-exp-toolbar-fallback";
        ourContainer.style.cssText = "position: fixed; bottom: 10px; right: 10px; padding: 8px; background: rgba(0,0,0,0.7); color: white; border-radius: 8px; z-index: 9999;";
        document.body.appendChild(ourContainer);
      }
      
      // 检查是否已经有统计容器
      let statsContainer = ourContainer.querySelector(".bili-exp-stats");
      if (!statsContainer) {
        // 如果没有,创建新的统计容器
        statsContainer = document.createElement("div");
        statsContainer.className = "bili-exp-stats";
        ourContainer.appendChild(statsContainer);
      }
      
      // 更新统计内容
      statsContainer.innerHTML = this.generateStatsHTML(statsData);
      return true;
    } catch (err) {
      console.error("[BilibiliExp] 渲染经验统计时出错", err);
      return false;
    }
  },
  
  // 插入图标样式
  insertIconStyle() {
    // 避免重复插入
    if (document.getElementById("bili-exp-style")) return;
    
    const style = document.createElement("style");
    style.id = "bili-exp-style";
    style.textContent = `
      .bili-exp-stats {
        margin-top: 10px;
        padding: 8px;
        background: rgba(0,0,0,0.03);
        border-radius: 6px;
        font-size: 14px;
        display: flex;
        flex-wrap: wrap;
        justify-content: space-around;
        width: 100%; /* 确保占用全宽 */
        box-sizing: border-box; /* 确保padding不增加宽度 */
      }
      .bilibili-exp-toolbar-fallback .bili-exp-stats {
        background: transparent;
        margin-top: 0;
      }
      .bili-exp-stats span {
        margin: 4px 4px; /* 减小水平间距 */
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        min-width: 90px; /* 减小最小宽度,更容易适应小空间 */
        max-width: 120px; /* 限制最大宽度 */
        padding: 2px 4px; /* 保持内边距 */
        flex-grow: 0; /* 防止自动拉伸 */
      }
      /* 特定统计项样式 */
      .bili-exp-stat-item {
        transition: all 0.2s ease;
        border-radius: 4px;
      }
      .bili-exp-stat-item:hover {
        background: rgba(0,0,0,0.05);
      }
      .bilibili-exp-toolbar-fallback .bili-exp-stat-item:hover {
        background: rgba(255,255,255,0.1);
      }
      @media screen and (max-width: 768px) {
        .bili-exp-stats span {
          min-width: 70px; /* 在小屏幕上进一步减小 */
          margin: 2px 2px;
        }
      }
      .bili-exp-stats span small {
        color: #999;
      }
      .bilibili-exp-toolbar-fallback .bili-exp-stats span small {
        color: #ddd;
      }
      .bili-exp-stats .completed {
        color: #fb7299;
      }
      .bili-exp-stats .incomplete {
        color: #666;
      }
      .bilibili-exp-toolbar-fallback .bili-exp-stats .incomplete {
        color: #ccc;
      }
      .bili-exp-stats i {
        margin-right: 5px;
        font-size: 18px;
      }
      /* 使用我们的自定义类修改工具栏样式 */
      .bili-exp-enhanced-toolbar {
        display: flex !important;
        flex-wrap: wrap !important;
        row-gap: 8px !important;
        align-items: center !important;
      }
      /* 针对插入到toolbar的统计容器的特殊样式 */
      .bili-exp-enhanced-toolbar .bili-exp-stats {
        margin-top: 4px !important;
        width: 100% !important;
        order: 999 !important; /* 确保显示在底部 */
      }
      .bili-exp-stats .icon-login:before { content: "\\e62b"; }
      .bili-exp-stats .icon-share:before { content: "\\e614"; }
      .bili-exp-stats .icon-play:before { content: "\\e616"; }
      .bili-exp-stats .icon-coin:before { content: "\\e60a"; }
      .bili-exp-stats .icon-total:before { content: "\\e619"; }
      .bili-exp-stats .icon-day:before { content: "\\e61e"; }
      .bili-exp-stats .icon-vip:before { content: "\\e630"; }
    `;
    document.head.appendChild(style);
    
    // 加载图标字体
    if (!document.querySelector('link[href*="font_1537779_4srood2g1uk.css"]')) {
      const link = document.createElement("link");
      link.rel = "stylesheet";
      link.href = "//at.alicdn.com/t/font_1537779_4srood2g1uk.css";
      document.head.appendChild(link);
    }
  },
  
  // 安全地应用样式到工具栏
  applyToolbarStyle(toolbar, selector) {
    try {
      // 首先检查toolbar是否有效
      if (!toolbar || typeof toolbar !== 'object' || !toolbar.classList) {
        console.error(`[BilibiliExp] 工具栏元素无效: ${selector}`);
        return false;
      }
      
      // 添加我们自己的标识类,而不是直接修改style
      toolbar.classList.add("bili-exp-enhanced-toolbar");
      
      // 记录我们修改了该元素,便于调试
      console.log(`[BilibiliExp] 为${selector}添加bili-exp-enhanced-toolbar类`);
      
      return true;
    } catch (err) {
      console.error(`[BilibiliExp] 应用工具栏样式失败:`, err);
      return false;
    }
  },
  
  // 准备统计数据
  prepareStatsData(rewardInfo) {
    // 参数校验
    if (!rewardInfo) {
      console.error("[BilibiliExp] 无法准备统计数据:奖励信息为空");
      return [];
    }
    
    let total = 0;
    const data = [];
    
    // 登录经验(检查字段是否存在)
    const hasLogin = typeof rewardInfo.login === 'boolean' ? rewardInfo.login : false;
    total += hasLogin ? 5 : 0;
    data.push({
      ok: hasLogin,
      name: "每日登录",
      text: (hasLogin ? 5 : 0) + "/5",
      className: "icon-login"
    });
    
    // 分享经验(检查字段是否存在)
    const hasShare = typeof rewardInfo.share === 'boolean' ? rewardInfo.share : false;
    total += hasShare ? 5 : 0;
    data.push({
      ok: hasShare,
      name: "分享视频",
      text: (hasShare ? 5 : 0) + "/5",
      className: "icon-share"
    });
    
    // 观看经验(检查字段是否存在)
    const hasWatch = typeof rewardInfo.watch === 'boolean' ? rewardInfo.watch : false;
    total += hasWatch ? 5 : 0;
    data.push({
      ok: hasWatch,
      name: "观看视频",
      text: (hasWatch ? 5 : 0) + "/5",
      className: "icon-play"
    });
    
    // 投币经验(检查字段是否存在)
    const coins = typeof rewardInfo.coins === 'number' ? rewardInfo.coins : 0;
    total += coins;
    data.push({
      ok: coins >= 50,
      name: "视频投币",
      text: coins + "/50",
      className: "icon-coin"
    });
    
    // 大会员观看经验(仅对大会员显示)
    if (rewardInfo.isVip) {
      const hasVipExp = rewardInfo.vipExp === true;
      const vipExpValue = hasVipExp ? 10 : 0;
      total += vipExpValue;
      data.push({
        ok: hasVipExp,
        name: "大会员观看",
        text: vipExpValue + "/10",
        className: "icon-vip",
        tooltip: hasVipExp ? "已获取大会员观看经验" : "大会员观看视频可获得10经验"
      });
    }
    
    // 总计
    data.push({
      ok: total >= (rewardInfo.isVip ? 75 : 65),
      name: "总计",
      text: total + "/" + (rewardInfo.isVip ? "75" : "65"),
      className: "icon-total"
    });
    
    // 升级天数(添加防御性检查)
    const daysToUpgrade = DailyReward.calculateDaysToUpgrade(rewardInfo);
    
    // 确保level_info存在且有current_level字段
    if (rewardInfo.level_info && typeof rewardInfo.level_info.current_level === 'number') {
      if (rewardInfo.level_info.current_level >= 6) {
        data.push({
          ok: true,
          name: "一个成熟的六级大佬",
          text: "六级辣",
          className: "icon-day"
        });
      } else {
        // 安全访问 current_level
        const nextLevel = (rewardInfo.level_info.current_level || 0) + 1;
        data.push({
          ok: false,
          name: `到 ${nextLevel} 级预计`,
          text: daysToUpgrade + " 天",
          className: "icon-day"
        });
      }
    } else {
      // 如果没有等级信息,显示占位符
      data.push({
        ok: false,
        name: "等级信息",
        text: "未知",
        className: "icon-day"
      });
    }
    
    return data;
  },
  
  // 生成经验统计HTML
  generateStatsHTML(data) {
    if (!data || !data.length) return "";
    
    return data.map(item => {
      const className = item.ok ? "completed" : "incomplete";
      // 构建基本HTML,添加更好的样式类
      let html = `<span class="${className} bili-exp-stat-item" title="${item.tooltip || ''}" ${item.onClick ? `onclick="${item.onClick}"` : ""}>
        <i class="${item.className} iconfont"></i>${item.name}: ${item.text}`;
      
      html += '</span>';
      return html;
    }).join("");
  }
};

// 工具函数
function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
  return '';
}

// AJAX请求封装
async function request(options) {
  return new Promise((resolve, reject) => {
    const defaultOptions = {
      xhrFields: { withCredentials: true },
      crossDomain: true
    };
    
    // 合并选项
    const opt = { ...defaultOptions, ...options };
    
    // 如果是POST,自动添加Content-Type
    if (opt.method === 'POST' && !opt.headers) {
      opt.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
    }
    
    // 发送请求
    GM.xmlHttpRequest({
      method: opt.method,
      url: opt.url,
      data: typeof opt.data === 'string' ? opt.data : new URLSearchParams(opt.data).toString(),
      headers: opt.headers,
      onload: function(response) {
        try {
          const res = JSON.parse(response.responseText);
          resolve(res);
        } catch (error) {
          reject(error);
        }
      },
      onerror: function(error) {
        reject(error);
      }
    });
  });
}

// 等待函数
function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 获取视频ID
function getVideoId() {
  try {
    // 新版B站页面中获取AID
    const videoData = unsafeWindow.__INITIAL_STATE__;
    if (videoData && videoData.aid) {
      return videoData.aid;
    }
    
    // 尝试从URL中获取
    const urlMatch = location.href.match(/\/video\/(\w+)/);
    if (urlMatch && unsafeWindow.bvid) {
      return unsafeWindow.aid || null;
    }
    
    return null;
  } catch (error) {
    console.error("[BilibiliExp] 获取视频ID失败", error);
    return null;
  }
}

// 主函数
async function main() {
  // 初始化WBI签名
  await WBI.init();
  
  // 获取视频ID
  const aid = getVideoId();
  if (!aid) {
    console.error("[BilibiliExp] 无法获取视频ID");
    return;
  }
  
  // 获取用户信息
  const userInfo = await UserInfo.get();
  if (!userInfo || !userInfo.isLogin) {
    console.error("[BilibiliExp] 用户未登录");
    return;
  }
  
  // 获取奖励状态
  const rewardStatus = await DailyReward.getStatus();
  if (!rewardStatus) {
    console.error("[BilibiliExp] 获取奖励状态失败");
    return;
  }
  
  // 自动分享
  if (CONFIG.AUTO_SHARE && !rewardStatus.share) {
    console.log("[BilibiliExp] 尝试分享视频");
    const shareResult = await ShareModule.share(aid);
    if (shareResult && shareResult.code === 0) {
      console.log("[BilibiliExp] 分享成功");
    }
  }
  
  // 自动投币
  if (CONFIG.AUTO_COIN && rewardStatus.coins < 50 && userInfo.money > 5) {
    const coinNum = Math.min(CONFIG.COIN_NUMBER, Math.floor((50 - rewardStatus.coins) / 10));
    if (coinNum > 0) {
      console.log(`[BilibiliExp] 尝试投${coinNum}个币`);
      const coinResult = await CoinModule.add(aid, coinNum);
      if (coinResult && coinResult.code === 0) {
        console.log(`[BilibiliExp] 投币成功`);
      }
    }
  }
  
  // 如果是大会员,尝试获取观看经验
  if (CONFIG.AUTO_VIP_EXP && userInfo.isVip) {
    console.log("[BilibiliExp] 检测到大会员账号,尝试获取观看经验");
    // 直接尝试获取经验,API会判断是否满足条件
    VIPExpModule.getVipExp().then(result => {
      if (result.code === 0) {
        console.log("[BilibiliExp] 大会员经验获取成功");
        // 刷新统计显示
        UIRenderer.renderExpStats();
      } else {
        console.log(`[BilibiliExp] 大会员经验获取结果: ${result.message || '未知'}`);
      }
    });
  }
  
  // 渲染经验统计 - 增加延迟和重试机制
  let attempts = 0;
  const maxAttempts = 10; // 增加最大尝试次数
  const attemptRender = async () => {
    await wait(3000); // 增加等待时间,确保页面加载完毕
    try {
      // 首先检查页面是否已经加载完成
      if (document.readyState !== "complete") {
        console.log(`[BilibiliExp] 页面尚未完全加载,等待后重试...`);
        attempts++;
        if (attempts < maxAttempts) {
          setTimeout(attemptRender, 2000); // 2秒后重试
        }
        return;
      }
      
      // 检查关键元素是否存在
      const pageLoaded = !!document.querySelector(".video-toolbar-container") || 
                        !!document.querySelector(".bpx-player-video-toolbar") ||
                        !!document.querySelector(".video-info-container");
      
      if (!pageLoaded) {
        console.log(`[BilibiliExp] 关键页面元素尚未加载,等待后重试...`);
        attempts++;
        if (attempts < maxAttempts) {
          setTimeout(attemptRender, 2000); // 2秒后重试
        }
        return;
      }
      
      const result = await UIRenderer.renderExpStats();
      if (result === false && attempts < maxAttempts) {
        console.log(`[BilibiliExp] 渲染尝试 ${attempts + 1}/${maxAttempts} 失败,将重试...`);
        attempts++;
        setTimeout(attemptRender, 3000); // 3秒后重试,增加等待时间
      }
    } catch (e) {
      console.error("[BilibiliExp] 渲染经验统计出错", e);
      if (attempts < maxAttempts) {
        console.log(`[BilibiliExp] 渲染尝试 ${attempts + 1}/${maxAttempts} 失败,将重试...`);
        attempts++;
        setTimeout(attemptRender, 3000); // 3秒后重试,增加等待时间
      }
    }
  };
  
  // 开始渲染尝试
  attemptRender();
}

// 页面加载完成后初始化
(function() {
  // 等待页面加载完成
  if (document.readyState === "complete" || document.readyState === "interactive") {
    console.log("[BilibiliExp] 页面已加载,延迟5秒执行初始化...");
    setTimeout(main, 5000); // 增加到5秒
  } else {
    console.log("[BilibiliExp] 等待页面加载...");
    window.addEventListener("load", () => {
      console.log("[BilibiliExp] 页面加载完成,延迟5秒执行初始化...");
      setTimeout(main, 5000); // 使用load事件,确保完全加载,并延迟5秒
    });
  }
})();