Cursor长效Token转换器

在Cursor Dashboard页面添加长效token转换功能

// ==UserScript==
// @name         Cursor长效Token转换器
// @namespace    https://cursor.com/
// @version      1.0.0
// @description  在Cursor Dashboard页面添加长效token转换功能
// @author       ltw
// @license      MIT
// @match        cursor.com/*dashboard*
// @grant        GM_setClipboard
// @grant        GM_notification
// @grant        GM_cookie
// @grant        GM.cookie
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// ==/UserScript==

;(function () {
  "use strict"

  // 等待页面完全加载
  function waitForPageLoad() {
    return new Promise(resolve => {
      if (document.readyState === "complete") {
        resolve()
      } else {
        window.addEventListener("load", resolve)
      }
    })
  }

  // 存储拦截到的session token
  let interceptedSessionToken = null
  let tokenExtracted = false

  // 从Cookie中获取WorkosCursorSessionToken
  function getTokenFromCookies() {
    return new Promise(resolve => {
      GM_cookie.list({ name: "WorkosCursorSessionToken" }, function (cookies, error) {
        if (error) {
          console.error("获取Cookie失败:", error)
          resolve(null)
          return
        }

        if (cookies && cookies.length > 0) {
          const token = cookies[0].value
          console.log("成功从Cookie获取到WorkosCursorSessionToken")
          resolve(token)
        } else {
          console.log("未找到WorkosCursorSessionToken Cookie")
          resolve(null)
        }
      })
    })
  }

  // 解析Cookie字符串
  function parseCookieString(cookieString) {
    try {
      const cookies = cookieString.split(";")
      for (let cookie of cookies) {
        const [name, value] = cookie.trim().split("=")
        if (name === "WorkosCursorSessionToken") {
          return decodeURIComponent(value)
        }
      }
      return null
    } catch (error) {
      console.error("解析Cookie失败:", error)
      return null
    }
  }

  // 获取session token(优先使用Cookie,失败时提供手动输入)
  async function getSessionToken() {
    // 如果已经获取到token,直接返回
    if (tokenExtracted && interceptedSessionToken) {
      return interceptedSessionToken
    }

    // 尝试从Cookie获取token
    const token = await getTokenFromCookies()
    if (token) {
      interceptedSessionToken = token
      tokenExtracted = true
      return token
    }

    // 如果自动获取失败,提供手动输入选项
    return await promptForManualToken()
  }

  // 手动输入token的提示
  function promptForManualToken() {
    return new Promise(resolve => {
      const input = prompt(
        "自动获取WorkosCursorSessionToken失败。\n\n" +
          "请手动输入WorkosCursorSessionToken值:\n" +
          "(您可以在浏览器开发者工具的Network标签页中找到此值)"
      )

      if (input && input.trim()) {
        resolve(input.trim())
      } else {
        resolve(null)
      }
    })
  }

  // 获取 Cursor API Token
  function getCursorToken(sessionToken) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: `https://token.cursorpro.com.cn/reftoken?token=${encodeURIComponent(sessionToken)}`,
        onload: function (response) {
          try {
            const data = JSON.parse(response.responseText)

            if (data.code === 0) {
              resolve({
                accessToken: data.data.accessToken,
                refreshToken: data.data.refreshToken,
                expireTime: data.data.expire_time,
                daysLeft: data.data.days_left,
                userId: data.data.user_id
              })
            } else {
              reject(new Error(data.msg || "获取失败"))
            }
          } catch (error) {
            console.error("解析响应失败:", error)
            reject(error)
          }
        },
        onerror: function (error) {
          console.error("获取Token失败:", error)
          reject(error)
        }
      })
    })
  }

  // 复制到剪贴板
  async function copyToClipboard(text) {
    try {
      if (navigator.clipboard && window.isSecureContext) {
        await navigator.clipboard.writeText(text)
        return true
      } else {
        // 降级方案
        const textArea = document.createElement("textarea")
        textArea.value = text
        textArea.style.position = "fixed"
        textArea.style.left = "-999999px"
        textArea.style.top = "-999999px"
        document.body.appendChild(textArea)
        textArea.focus()
        textArea.select()
        const result = document.execCommand("copy")
        document.body.removeChild(textArea)
        return result
      }
    } catch (error) {
      console.error("复制到剪贴板失败:", error)
      return false
    }
  }

  // 显示Toast通知
  function showToast(message, type = "info") {
    // 移除已存在的toast
    const existingToast = document.getElementById("cursor-token-toast")
    if (existingToast) {
      existingToast.remove()
    }

    const toast = document.createElement("div")
    toast.id = "cursor-token-toast"
    toast.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: ${
              type === "success" ? "#4CAF50" : type === "error" ? "#f44336" : "#2196F3"
            };
            color: white;
            padding: 12px 20px;
            border-radius: 4px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            z-index: 10000;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            font-size: 14px;
            max-width: 300px;
            word-wrap: break-word;
            opacity: 0;
            transform: translateX(100%);
            transition: all 0.3s ease;
        `
    toast.textContent = message
    document.body.appendChild(toast)

    // 动画显示
    setTimeout(() => {
      toast.style.opacity = "1"
      toast.style.transform = "translateX(0)"
    }, 10)

    // 自动隐藏
    setTimeout(() => {
      toast.style.opacity = "0"
      toast.style.transform = "translateX(100%)"
      setTimeout(() => {
        if (toast.parentNode) {
          toast.parentNode.removeChild(toast)
        }
      }, 300)
    }, 3000)
  }

  // 创建浮动按钮
  function createFloatingButton() {
    const button = document.createElement("button")
    button.id = "cursor-token-converter-btn"
    button.innerHTML = `
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="margin-right: 8px;">
        <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
        <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
        <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
      </svg>
      转换为长效Token
    `
    button.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            padding: 14px 24px;
            border-radius: 12px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 600;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3), 0 4px 16px rgba(102, 126, 234, 0.2);
            z-index: 9999;
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            user-select: none;
            display: flex;
            align-items: center;
            justify-content: center;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.1);
        `

    // 悬停效果
    button.addEventListener("mouseenter", () => {
      button.style.transform = "translateY(-3px) scale(1.02)"
      button.style.boxShadow =
        "0 12px 40px rgba(102, 126, 234, 0.4), 0 8px 24px rgba(102, 126, 234, 0.3)"
      button.style.background = "linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%)"
    })

    button.addEventListener("mouseleave", () => {
      button.style.transform = "translateY(0) scale(1)"
      button.style.boxShadow =
        "0 8px 32px rgba(102, 126, 234, 0.3), 0 4px 16px rgba(102, 126, 234, 0.2)"
      button.style.background = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
    })

    // 点击事件
    button.addEventListener("click", async () => {
      button.disabled = true
      button.innerHTML = `
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="margin-right: 8px; animation: spin 1s linear infinite;">
          <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" opacity="0.3"/>
          <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-dasharray="60" stroke-dashoffset="60">
            <animate attributeName="stroke-dashoffset" dur="1s" values="60;0" repeatCount="indefinite"/>
          </path>
        </svg>
        转换中...
      `
      button.style.opacity = "0.8"
      button.style.transform = "scale(0.98)"

      try {
        // 获取sessionToken
        const sessionToken = await getSessionToken()
        if (!sessionToken) {
          showToast("未找到WorkosCursorSessionToken,请确保已登录Cursor", "error")
          return
        }

        // 调用API获取长效token
        const tokenData = await getCursorToken(sessionToken)

        // 格式化token信息
        const tokenInfo = `Access Token: ${tokenData.accessToken}\nRefresh Token: ${tokenData.refreshToken}\n过期时间: ${tokenData.expireTime}\n剩余天数: ${tokenData.daysLeft}天\n用户ID: ${tokenData.userId}`

        // 复制到剪贴板
        const copySuccess = await copyToClipboard(tokenData.accessToken)

        if (copySuccess) {
          showToast(
            `转换成功!Access Token已复制到剪贴板\n剩余天数: ${tokenData.daysLeft}天`,
            "success"
          )
        } else {
          showToast("转换成功,但复制到剪贴板失败,请手动复制", "error")
        }
      } catch (error) {
        console.error("Token转换失败:", error)
        showToast(`转换失败: ${error.message}`, "error")
      } finally {
        // 恢复按钮状态
        button.disabled = false
        button.innerHTML = `
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="margin-right: 8px;">
            <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
            <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
            <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
          </svg>
          转换为长效Token
        `
        button.style.opacity = "1"
        button.style.transform = "scale(1)"
      }
    })

    return button
  }

  // 添加CSS动画样式
  function addAnimationStyles() {
    const style = document.createElement("style")
    style.textContent = `
      @keyframes spin {
        from { transform: rotate(0deg); }
        to { transform: rotate(360deg); }
      }
      
      #cursor-token-converter-btn {
        animation: none;
      }
      
      #cursor-token-converter-btn:hover {
        animation: none;
      }
    `
    document.head.appendChild(style)
  }

  // 主函数
  async function init() {
    try {
      await waitForPageLoad()

      // 检查是否在正确的页面
      if (
        !window.location.href.includes("cursor.com") ||
        !window.location.href.includes("dashboard")
      ) {
        return
      }

      // 添加动画样式
      addAnimationStyles()

      // 创建并添加按钮
      const button = createFloatingButton()
      document.body.appendChild(button)

      console.log("Cursor长效Token转换器已加载")
    } catch (error) {
      console.error("初始化失败:", error)
    }
  }

  // 启动脚本
  init()
})()