From 3d4e6b4c00710eadd50ba3a5b00173778aeb7da9 Mon Sep 17 00:00:00 2001 From: Neo Date: Sun, 8 Feb 2026 17:05:39 +0800 Subject: [PATCH] refactor: add detection module (#163) --- apps/extension/package.json | 3 +- apps/extension/src/background.js | 1499 +------------------ packages/detection/index.js | 376 +---- packages/detection/src/configs.js | 361 +++++ packages/detection/src/detect.js | 52 + packages/detection/src/detectors.js | 644 ++++++++ packages/detection/src/platforms/csdn.js | 53 + packages/detection/src/platforms/oschina.js | 75 + packages/detection/src/utils.js | 121 ++ pnpm-lock.yaml | 3 + 10 files changed, 1317 insertions(+), 1870 deletions(-) create mode 100644 packages/detection/src/configs.js create mode 100644 packages/detection/src/detect.js create mode 100644 packages/detection/src/detectors.js create mode 100644 packages/detection/src/platforms/csdn.js create mode 100644 packages/detection/src/platforms/oschina.js create mode 100644 packages/detection/src/utils.js diff --git a/apps/extension/package.json b/apps/extension/package.json index b76cfab..ed2826c 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -15,7 +15,8 @@ "lint": "web-ext lint --source-dir ./dist" }, "dependencies": { - "@cose/core": "workspace:*" + "@cose/core": "workspace:*", + "@cose/detection": "workspace:*" }, "devDependencies": { "cac": "^6.7.14", diff --git a/apps/extension/src/background.js b/apps/extension/src/background.js index ae53d86..f73f4b4 100644 --- a/apps/extension/src/background.js +++ b/apps/extension/src/background.js @@ -281,1508 +281,15 @@ async function checkAllPlatformsProgressive(platforms, tabId) { } } +import { detectUser } from '@cose/detection' + // 检查单个平台登录状态 async function checkPlatformLogin(platform) { if (!platform || !platform.id) { return { loggedIn: false, error: '无效的平台配置' } } - const config = LOGIN_CHECK_CONFIG[platform.id] - if (!config) { - return { loggedIn: false, error: '未配置检测' } - } - - // 支付宝开放平台特殊处理:从 storage 读取缓存的用户信息(由 content script 在访问支付宝时缓存) - if (platform.id === 'alipayopen') { - try { - // 从 storage 读取缓存的用户信息 - const stored = await chrome.storage.local.get('alipayopen_user') - const cachedUser = stored.alipayopen_user - - if (cachedUser && cachedUser.loggedIn) { - // 检查缓存是否过期(1小时) - const cacheAge = Date.now() - (cachedUser.cachedAt || 0) - const maxAge = 1 * 60 * 60 * 1000 // 1 hour - - if (cacheAge < maxAge) { - console.log(`[COSE] alipayopen 从缓存读取用户信息:`, cachedUser.username) - return { - loggedIn: true, - username: cachedUser.username || '', - avatar: cachedUser.avatar || '' - } - } else { - console.log(`[COSE] alipayopen 缓存已过期`) - // 清除过期缓存 - await chrome.storage.local.remove('alipayopen_user') - } - } - - // 尝试从已打开的支付宝页面获取用户信息并缓存 - let tabs = await chrome.tabs.query({ url: 'https://open.alipay.com/*' }) - if (tabs.length === 0) { - tabs = await chrome.tabs.query({ url: 'https://*.alipay.com/*' }) - } - - if (tabs.length > 0) { - try { - // 在已打开的页面上下文中调用 API - const results = await chrome.scripting.executeScript({ - target: { tabId: tabs[0].id }, - func: async () => { - try { - const response = await fetch('https://developerportal.alipay.com/octopus/service.do', { - method: 'POST', - credentials: 'include', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', - }, - body: 'data=%5B%7B%7D%5D&serviceName=alipay.open.developerops.forum.user.query', - }) - if (!response.ok) return null - return await response.json() - } catch (e) { - return null - } - } - }) - - const data = results?.[0]?.result - console.log(`[COSE] alipayopen API 数据:`, data) - - if (data?.stat === 'ok' && data?.data?.isLoginUser === 1) { - const username = data.data.nickname || '' - const avatar = data.data.avatar || '' - - // 缓存用户信息 - await chrome.storage.local.set({ - alipayopen_user: { - loggedIn: true, - username, - avatar, - cachedAt: Date.now() - } - }) - - console.log(`[COSE] alipayopen 用户信息:`, username, avatar ? '有头像' : '无头像') - return { loggedIn: true, username, avatar } - } - } catch (e) { - console.log(`[COSE] alipayopen 从页面获取用户信息失败:`, e.message) - } - } - - console.log(`[COSE] alipayopen 未检测到登录状态,请先访问支付宝开放平台`) - return { loggedIn: false } - } catch (e) { - console.log(`[COSE] alipayopen 检测失败:`, e.message) - return { loggedIn: false, error: e.message } - } - } - - // 微博特殊处理:通过 cookie 检测登录,通过 fetch HTML 获取用户信息 - if (platform.id === 'weibo') { - try { - // 检查 card.weibo.com 的 SUBP cookie - const subpCookie = await chrome.cookies.get({ - url: 'https://card.weibo.com', - name: 'SUBP' - }) - - // 也检查 ALF cookie - const alfCookie = await chrome.cookies.get({ - url: 'https://card.weibo.com', - name: 'ALF' - }) - - console.log(`[COSE] weibo cookies: SUBP=${!!subpCookie}, ALF=${!!alfCookie}`) - - if (!subpCookie && !alfCookie) { - console.log(`[COSE] weibo 未找到登录 cookie,未登录`) - return { loggedIn: false } - } - - // 有 cookie,通过 fetch HTML 获取用户信息 - let username = '' - let avatar = '' - - try { - // 获取所有相关 cookies 并手动添加到请求 - const weiboCookies = await chrome.cookies.getAll({ domain: '.weibo.com' }) - const cardCookies = await chrome.cookies.getAll({ domain: 'card.weibo.com' }) - const sinaCookies = await chrome.cookies.getAll({ domain: '.sina.com.cn' }) - const allCookies = [...weiboCookies, ...cardCookies, ...sinaCookies] - const cookieString = allCookies.map(c => `${c.name}=${c.value}`).join('; ') - - console.log(`[COSE] weibo 获取到 ${allCookies.length} 个 cookies`) - - const response = await fetch('https://card.weibo.com/article/v5/editor', { - method: 'GET', - headers: { - 'Cookie': cookieString, - }, - credentials: 'include', - }) - const html = await response.text() - - await logToStorage(`[COSE] weibo HTML 长度: ${html.length}`) - if (html.length < 1000) { - await logToStorage(`[COSE] weibo HTML content snippet:`, html) - } else { - await logToStorage(`[COSE] weibo HTML content starts with:`, html.substring(0, 500)) - } - - // 从 HTML 中提取用户名 - const nickMatch = html.match(/"nick"\s*:\s*"([^"]+)"/) - if (nickMatch) { - username = nickMatch[1] - } else { - // 深度查找 nick - const altNickMatch = html.match(/\\"nick\\"\s*:\s*\\"([^\\"]+)\\"/) - if (altNickMatch) { - username = altNickMatch[1] - await logToStorage(`[COSE] weibo Found nick via escaped regex: ${username}`) - } else { - await logToStorage(`[COSE] weibo Failed to find nick in HTML. Snip:`, html.substring(html.indexOf('nick') - 20, html.indexOf('nick') + 100)) - } - } - - // 从 HTML 中提取头像 - const avatarMatch = html.match(/"avatar_large"\s*:\s*"([^"]+)"/) - if (avatarMatch) { - // 处理转义的 URL - let rawAvatar = avatarMatch[1].replace(/\\/g, '') - const avatarUrl = rawAvatar - try { - await logToStorage(`[COSE] weibo fetching avatar: ${avatarUrl}`) - const avatarRes = await fetch(avatarUrl) - if (avatarRes.ok) { - const blob = await avatarRes.blob() - avatar = await new Promise((resolve) => { - const reader = new FileReader() - reader.onloadend = () => resolve(reader.result) - reader.readAsDataURL(blob) - }) - await logToStorage(`[COSE] weibo avatar converted successfully (length: ${avatar.length})`) - } else { - await logToStorage(`[COSE] weibo avatar fetch failed with status: ${avatarRes.status}`) - avatar = avatarUrl - } - } catch (e) { - await logToStorage(`[COSE] weibo avatar fetch failed: ${e.message}`) - avatar = avatarUrl - } - } else { - // 深度查找 avatar_large - const altAvatarMatch = html.match(/\\"avatar_large\\"\s*:\s*\\"([^\\"]+)\\"/) - if (altAvatarMatch) { - let rawAvatar = altAvatarMatch[1].replace(/\\\\\\\//g, '/') - if (rawAvatar.includes('sinaimg.cn')) { - avatar = rawAvatar.split('?')[0] - } else { - avatar = rawAvatar - } - await logToStorage(`[COSE] weibo Found avatar via escaped regex: ${avatar}`) - - // Try fetching if it's not converted yet - if (avatar && avatar.startsWith('http')) { - try { - await logToStorage(`[COSE] weibo fetching avatar (alt): ${avatar}`) - const avatarRes = await fetch(avatar) - if (avatarRes.ok) { - const blob = await avatarRes.blob() - avatar = await new Promise((resolve) => { - const reader = new FileReader() - reader.onloadend = () => resolve(reader.result) - reader.readAsDataURL(blob) - }) - await logToStorage(`[COSE] weibo avatar converted successfully (alt) (length: ${avatar.length})`) - } - } catch (e) { - await logToStorage(`[COSE] weibo avatar fetch error (alt): ${e.message}`) - } - } - } else { - await logToStorage(`[COSE] weibo Failed to find avatar_large in HTML`) - } - } - - await logToStorage(`[COSE] weibo 用户信息: ${username} ${avatar ? (avatar.startsWith('data:') ? 'Base64头像' : 'URL头像') : '无头像'}`) - } catch (e) { - console.log(`[COSE] weibo 获取用户详情失败:`, e.message) - } - - // 如果没有获取到用户名,说明实际上未登录或登录已过期 - if (!username) { - console.log(`[COSE] weibo 未获取到用户名,视为未登录`) - return { loggedIn: false } - } - - return { loggedIn: true, username, avatar } - } catch (e) { - console.log(`[COSE] weibo 检测失败:`, e.message) - return { loggedIn: false } - } - } - - - - - - - console.log(`[COSE] checkPlatformLogin checking: '${platform.id}'`) - - if (platform.id === 'twitter') { - console.log(`[COSE] Entering Twitter block for ${platform.id}`) - return await checkTwitterLogin(platform) - } - - if (config.useCookie) { - return await checkLoginByCookie(platform.id, config) - } - - // 微信公众号特殊处理:通过已打开的页面或 fetch 首页检测登录状态 - if (platform.id === 'wechat') { - try { - // 先检查缓存 - const stored = await chrome.storage.local.get('wechat_user') - const cachedUser = stored.wechat_user - - if (cachedUser && cachedUser.loggedIn) { - const cacheAge = Date.now() - (cachedUser.cachedAt || 0) - const maxAge = 1 * 60 * 60 * 1000 // 1 hour - - if (cacheAge < maxAge) { - console.log(`[COSE] wechat 从缓存读取:`, cachedUser.username) - return { - loggedIn: true, - username: cachedUser.username || '', - avatar: cachedUser.avatar || '' - } - } else { - await chrome.storage.local.remove('wechat_user') - } - } - - // 优先尝试在已打开的微信公众号页面中检测 - const tabs = await chrome.tabs.query({ url: 'https://mp.weixin.qq.com/*' }) - if (tabs.length > 0) { - try { - const results = await chrome.scripting.executeScript({ - target: { tabId: tabs[0].id }, - func: () => { - // 从 window.wx.data 读取用户信息 - const wxData = window.wx?.data - if (wxData && wxData.nick_name) { - return { - loggedIn: true, - username: wxData.nick_name || wxData.user_name || '', - avatar: wxData.head_img || '', - token: wxData.t || '' - } - } - return null - } - }) - - const result = results?.[0]?.result - if (result && result.loggedIn) { - // 缓存结果 - const userInfo = { - ...result, - cachedAt: Date.now() - } - await chrome.storage.local.set({ wechat_user: userInfo }) - console.log(`[COSE] wechat 从页面检测成功:`, userInfo.username) - return { - loggedIn: true, - username: userInfo.username || '', - avatar: userInfo.avatar || '' - } - } - } catch (e) { - console.log(`[COSE] wechat 页面脚本执行失败:`, e.message) - } - } - - // 备用方案:fetch 首页并解析 HTML - try { - const response = await fetch('https://mp.weixin.qq.com/', { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'text/html' - } - }) - const html = await response.text() - - // 检查是否需要登录 - if (html.includes('请使用微信扫描') || html.includes('扫码登录')) { - console.log(`[COSE] wechat 未登录(需要扫码)`) - return { loggedIn: false } - } - - // 尝试从 HTML 提取用户信息 - const nickMatch = html.match(/nick_name\s*[:=]\s*["']([^"']+)["']/) - const avatarMatch = html.match(/head_img\s*[:=]\s*["']([^"']+)["']/) - - if (nickMatch) { - const username = nickMatch[1] - const avatar = avatarMatch ? avatarMatch[1] : '' - - // 缓存结果 - await chrome.storage.local.set({ - wechat_user: { - loggedIn: true, - username, - avatar, - cachedAt: Date.now() - } - }) - - console.log(`[COSE] wechat 从 HTML 检测成功:`, username) - return { loggedIn: true, username, avatar } - } - } catch (e) { - console.log(`[COSE] wechat fetch 失败:`, e.message) - } - - console.log(`[COSE] wechat 未登录或检测失败`) - return { loggedIn: false } - } catch (e) { - console.log(`[COSE] wechat 检测失败:`, e.message) - return { loggedIn: false } - } - } - - // 小红书特殊处理:直接使用 scripting API 检测登录状态 - if (platform.id === 'xiaohongshu') { - try { - // 先检查缓存 - const stored = await chrome.storage.local.get('xiaohongshu_user') - const cachedUser = stored.xiaohongshu_user - - if (cachedUser && cachedUser.loggedIn) { - const cacheAge = Date.now() - (cachedUser.cachedAt || 0) - const maxAge = 7 * 24 * 60 * 60 * 1000 // 7 days - - if (cacheAge < maxAge) { - console.log(`[COSE] xiaohongshu 从缓存读取:`, cachedUser.username) - return { - loggedIn: true, - username: cachedUser.username || '', - avatar: cachedUser.avatar || '' - } - } else { - await chrome.storage.local.remove('xiaohongshu_user') - } - } - - // 缓存无效,尝试在已打开的小红书页面中检测 - const tabs = await chrome.tabs.query({ url: 'https://creator.xiaohongshu.com/*' }) - if (tabs.length > 0) { - const results = await chrome.scripting.executeScript({ - target: { tabId: tabs[0].id }, - func: async () => { - try { - const response = await fetch('https://creator.xiaohongshu.com/api/galaxy/user/info', { - method: 'GET', - credentials: 'include', - headers: { 'Accept': 'application/json' } - }) - - if (!response.ok) return null - - const data = await response.json() - if (data?.success === true && data?.code === 0 && data?.data?.userId) { - return { - loggedIn: true, - username: data.data.userName || data.data.redId || '', - avatar: data.data.userAvatar || '', - userId: data.data.userId - } - } - return null - } catch (e) { - return null - } - } - }) - - const result = results?.[0]?.result - if (result && result.loggedIn) { - // 缓存结果 - const userInfo = { - ...result, - cachedAt: Date.now() - } - await chrome.storage.local.set({ xiaohongshu_user: userInfo }) - console.log(`[COSE] xiaohongshu 检测成功:`, userInfo.username) - return { - loggedIn: true, - username: userInfo.username || '', - avatar: userInfo.avatar || '' - } - } - } - - console.log(`[COSE] xiaohongshu 未登录,请先访问小红书创作者中心`) - return { loggedIn: false } - } catch (e) { - console.log(`[COSE] xiaohongshu 检测失败:`, e.message) - return { loggedIn: false } - } - } - - // 电子发烧友特殊处理:直接调用 API 检测登录状态 - if (platform.id === 'elecfans') { - try { - // 先获取 elecfans 的登录 cookie - const authCookie = await chrome.cookies.get({ - url: 'https://www.elecfans.com', - name: 'auth' - }) - - const authWwwCookie = await chrome.cookies.get({ - url: 'https://www.elecfans.com', - name: 'auth_www' - }) - - console.log(`[COSE] elecfans cookies: auth=${!!authCookie}, auth_www=${!!authWwwCookie}`) - - if (!authCookie && !authWwwCookie) { - console.log(`[COSE] elecfans 未找到登录 cookie,未登录`) - return { loggedIn: false } - } - - // 直接调用 API 获取用户信息 - try { - const response = await fetch('https://www.elecfans.com/webapi/passport/checklogin?_=' + Date.now(), { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'application/json, text/javascript, */*; q=0.01', - } - }) - - if (!response.ok) { - console.log(`[COSE] elecfans API 响应错误:`, response.status) - return { loggedIn: true, username: '', avatar: '' } - } - - const data = await response.json() - console.log(`[COSE] elecfans API 数据:`, data) - - // API 返回格式: {"uid":"6997925","username":"jf_50332692","avatar":"https://..."} - if (data && data.uid) { - const username = data.username || '' - const avatar = data.avatar || '' - console.log(`[COSE] elecfans 用户信息:`, username, avatar ? '有头像' : '无头像') - return { loggedIn: true, username, avatar } - } else { - // 有 cookie 但 API 返回无用户,仍然认为已登录 - console.log(`[COSE] elecfans API 返回无用户数据,但有 cookie`) - return { loggedIn: true, username: '', avatar: '' } - } - } catch (e) { - console.log(`[COSE] elecfans API 调用失败:`, e.message) - // API 失败但有 cookie,仍然认为已登录 - return { loggedIn: true, username: '', avatar: '' } - } - } catch (e) { - console.log(`[COSE] elecfans 检测失败:`, e.message) - return { loggedIn: false } - } - } - - // 少数派特殊处理:通过 localStorage 检测登录状态(类似 Sohu) - // 少数派特殊处理:通过 Cookie 获取 Token 调用 API - if (platform.id === 'sspai') { - try { - console.log(`[COSE] sspai 开始检测 via API`) - const cookie = await chrome.cookies.get({ url: 'https://sspai.com', name: 'sspai_jwt_token' }) - if (cookie && cookie.value) { - const token = cookie.value - try { - const response = await fetch('https://sspai.com/api/v1/user/info/get', { - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/json' - } - }) - - if (!response.ok) { - await logToStorage(`[COSE] sspai API 响应错误: ${response.status}`) - return { loggedIn: false } - } - - const data = await response.json() - await logToStorage(`[COSE] sspai API 数据:`, data) - - if (data && data.data && data.data.nickname) { - const userInfo = data.data - let avatar = userInfo.avatar || '' - - // 获取头像并转为 base64 解决防盗链 - if (avatar) { - try { - await logToStorage(`[COSE] sspai fetching avatar: ${avatar}`) - const avatarRes = await fetch(avatar) - if (avatarRes.ok) { - const blob = await avatarRes.blob() - avatar = await new Promise((resolve) => { - const reader = new FileReader() - reader.onloadend = () => resolve(reader.result) - reader.readAsDataURL(blob) - }) - await logToStorage(`[COSE] sspai avatar converted successfully (length: ${avatar.length})`) - } - } catch (e) { - await logToStorage(`[COSE] sspai avatar fetch failed: ${e.message}`) - } - } - - await logToStorage(`[COSE] sspai API 检测成功: ${userInfo.nickname}`) - return { - loggedIn: true, - username: userInfo.nickname, - avatar: avatar - } - } - } catch (e) { - await logToStorage(`[COSE] sspai API fetch error: ${e.message}`) - } - } - - console.log(`[COSE] sspai API 检测失败 (无有效 Token/Cookie)`) - return { loggedIn: false } - } catch (e) { - console.log(`[COSE] sspai 检测失败:`, e.message) - return { loggedIn: false } - } - } - - // Twitter specialized detection - // Twitter specialized detection - async function checkTwitterLogin(platform) { - try { - await logToStorage(`[COSE] Twitter specialized detection started`) - - // 1. Get ct0 cookie - const ct0Cookie = await chrome.cookies.get({ url: 'https://x.com', name: 'ct0' }) - if (!ct0Cookie) { - await logToStorage(`[COSE] Twitter detection failed: ct0 cookie not found`) - return { loggedIn: false } - } - - // 2. Constants - const bearerToken = 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA' - - let screenName = null - let avatar = null - - // 3. Try Login via settings API (Fast Path) - try { - const settingsResponse = await fetch('https://api.x.com/1.1/account/settings.json', { - headers: { - 'authorization': bearerToken, - 'x-csrf-token': ct0Cookie.value - } - }) - - if (settingsResponse.ok) { - const settingsData = await settingsResponse.json() - screenName = settingsData.screen_name - await logToStorage(`[COSE] Twitter settings API success: ${screenName}`) - } else { - await logToStorage(`[COSE] Twitter settings API failed: ${settingsResponse.status}, trying fallback`) - } - } catch (e) { - await logToStorage(`[COSE] Twitter API error: ${e.message}`) - } - - - // 4. Fallback / Avatar Scraping - // If we don't have screenName (API failed) or we just need avatar (API doesn't return it easily without more calls) - // We explicitly fetch home to scrape. - - if (!screenName || !avatar) { - try { - const homeResponse = await fetch('https://x.com/home') - const homeText = await homeResponse.text() - - // Scrape Avatar - const avatarMatch = homeText.match(/"profile_image_url_https":"([^"]+)"/) - if (avatarMatch && avatarMatch[1]) { - avatar = avatarMatch[1].replace(/\\/g, '') - } - - // Scrape Screen Name if missing - if (!screenName) { - // Try to find screen_name in the initial state - // often format: "screen_name":"username" - const nameMatch = homeText.match(/"screen_name":"([^"]+)"/) - if (nameMatch && nameMatch[1]) { - screenName = nameMatch[1] - } - } - } catch (e) { - await logToStorage(`[COSE] Twitter scraping failed: ${e.message}`) - } - } - - if (!screenName) { - await logToStorage(`[COSE] Twitter detection failed: screen_name not found via API or Scraping`) - return { loggedIn: false } - } - - return { - loggedIn: true, - username: screenName, - avatar: avatar - } - - } catch (error) { - await logToStorage(`[COSE] Twitter detection error: ${error.message}`) - return { loggedIn: false, error: error.message } - } - } - - // Special handling for Twitter - if (platform.id === 'twitter') { - return await checkTwitterLogin(platform) - } - - try { - await logToStorage(`[COSE] ${platform.id} 开始 API 检测: ${config.api}`) - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), 8000) - const response = await fetch(config.api, { - method: config.method || 'GET', - credentials: 'include', - headers: { - 'Accept': config.isHtml ? 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' : 'application/json', - 'Cache-Control': 'no-cache' - }, - signal: controller.signal, - }) - clearTimeout(timeoutId) - await logToStorage(`[COSE] ${platform.id} API 响应状态: ${response.status}`) - - let data = null - const contentType = response.headers.get('content-type') || '' - if (config.isHtml) { - // 明确指定为 HTML 响应时,使用 text() 解析 - try { data = await response.text() } catch (e) { data = '' } - } else { - // 其他情况尝试 JSON 解析 - try { data = await response.json() } catch (e) { data = null } - } - await logToStorage(`[COSE] ${platform.id} API 数据:`, data) - - const loggedIn = config.checkLogin(data) - await logToStorage(`[COSE] ${platform.id} checkLogin 结果: ${loggedIn} type: ${typeof loggedIn}`) - if (loggedIn && config.getUserInfo) { - const userInfo = config.getUserInfo(data) - console.log(`[COSE] ${platform.id} 用户信息:`, userInfo) - return { loggedIn: true, ...userInfo } - } - return { loggedIn: !!loggedIn } - } catch (error) { - await logToStorage(`[COSE] ${platform.id} API 检测失败: ${error.message}`) - return { loggedIn: false, error: error.message } - } + return await detectUser(platform.id) } - -// 通过 Cookie 检测登录状态 -async function checkLoginByCookie(platformId, config) { - try { - // 直接按名称查找 cookie - const cookieMap = {} - for (const name of config.cookieNames) { - const cookie = await chrome.cookies.get({ - url: config.cookieUrl || `https://${config.cookieDomain}`, - name: name - }) - if (cookie) { - cookieMap[name] = cookie.value - } - } - - console.log(`[COSE] ${platformId} 找到的cookies:`, Object.keys(cookieMap)) - - const hasLoginCookie = config.cookieNames.some(name => cookieMap[name]) - - if (!hasLoginCookie) { - console.log(`[COSE] ${platformId} 未找到登录 cookie`) - return { loggedIn: false } - } - - // 自定义 cookie 值检测逻辑(如 InfoQ) - if (config.customCheck && config.checkCookieValue) { - console.log(`[COSE] ${platformId} 使用自定义 cookie 检测`) - const result = config.checkCookieValue(cookieMap) - console.log(`[COSE] ${platformId} 自定义检测结果:`, result) - return result - } - - let username = '' - let avatar = '' - - // 如果配置了从 cookie 获取用户名 - if (config.getUsernameFromCookie && config.usernameCookie) { - username = decodeURIComponent(cookieMap[config.usernameCookie] || '') - } - - - // 使用平台配置的 fetchAvatar 回调获取头像 - if (config.fetchAvatar && typeof config.fetchAvatar === 'function') { - try { - const fetchedAvatar = await config.fetchAvatar(cookieMap) - if (fetchedAvatar) { - avatar = fetchedAvatar - console.log(`[COSE] ${platformId} 找到头像:`, avatar) - } - } catch (e) { - console.log(`[COSE] ${platformId} 获取头像失败:`, e.message) - } - } - - // 百家号特殊处理:通过 API 获取用户信息 - if (platformId === 'baijiahao') { - try { - // 百家号的 API 需要从页面获取 token,这里直接请求一个不需要 token 的页面 - // 然后从返回的 HTML 中提取用户信息 - const response = await fetch('https://baijiahao.baidu.com/builder/app/appinfo', { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'application/json', - } - }) - const data = await response.json() - - if (data.errno === 0 && data.data?.user?.username) { - username = data.data.user.username || data.data.user.name - avatar = data.data.user.avatar || '' - if (avatar.startsWith('//')) { - avatar = 'https:' + avatar - } - console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') - return { loggedIn: true, username, avatar } - } else { - console.log(`[COSE] ${platformId} API 返回未登录状态`) - return { loggedIn: false } - } - } catch (e) { - console.log(`[COSE] ${platformId} 获取用户信息失败:`, e.message) - // 如果 API 失败,但有 BDUSS cookie,仍然认为已登录 - return { loggedIn: true, username: '', avatar: '' } - } - } - - // 网易号特殊处理:通过 navinfo.do API 获取用户信息 - if (platformId === 'wangyihao') { - try { - const timestamp = Date.now() - const response = await fetch(`https://mp.163.com/wemedia/navinfo.do?_=${timestamp}`, { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'application/json', - } - }) - const data = await response.json() - - if (data.code === 1 && data.data?.tname) { - username = data.data.tname - avatar = data.data.icon || '' - console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') - return { loggedIn: true, username, avatar } - } else { - console.log(`[COSE] ${platformId} API 返回未登录状态`) - return { loggedIn: false } - } - } catch (e) { - console.log(`[COSE] ${platformId} 获取用户信息失败:`, e.message) - // 如果 API 失败,但有登录 cookie,仍然认为已登录 - return { loggedIn: true, username: '', avatar: '' } - } - } - - // 搜狐号特殊处理:通过 API 获取用户信息 - if (platformId === 'sohu') { - try { - // 检查 ppinf cookie 判断是否登录 - const ppinfCookie = await chrome.cookies.get({ - url: 'https://mp.sohu.com', - name: 'ppinf' - }) - - if (!ppinfCookie || !ppinfCookie.value) { - console.log(`[COSE] ${platformId} 未找到 ppinf cookie,未登录`) - return { loggedIn: false } - } - - // 直接调用 API 获取用户信息 - try { - const response = await fetch('https://mp.sohu.com/mpbp/bp/account/list', { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'application/json', - } - }) - const data = await response.json() - - if (data.success && data.data?.data?.[0]?.accounts?.[0]) { - const account = data.data.data[0].accounts[0] - let avatar = account.avatar || '' - if (avatar.startsWith('//')) { - avatar = 'https:' + avatar - } - console.log(`[COSE] ${platformId} 用户信息:`, account.nickName, avatar ? '有头像' : '无头像') - return { - loggedIn: true, - username: account.nickName, - avatar - } - } else { - console.log(`[COSE] ${platformId} API 返回无用户数据`) - return { loggedIn: true, username: '', avatar: '' } - } - } catch (e) { - console.log(`[COSE] ${platformId} API 调用失败:`, e.message) - // API 失败但有 cookie,仍然认为已登录 - return { loggedIn: true, username: '', avatar: '' } - } - } catch (e) { - console.log(`[COSE] ${platformId} 获取用户信息失败:`, e.message) - return { loggedIn: false } - } - } - - // 阿里云开发者社区特殊处理:通过 API 获取用户信息 - if (platformId === 'aliyun') { - try { - // 检查 login_aliyunid_ticket cookie 判断是否登录 - const ticketCookie = await chrome.cookies.get({ - url: 'https://developer.aliyun.com', - name: 'login_aliyunid_ticket' - }) - - if (!ticketCookie || !ticketCookie.value) { - console.log(`[COSE] ${platformId} 未找到 login_aliyunid_ticket cookie,未登录`) - return { loggedIn: false } - } - - // 调用 API 获取用户信息 - const response = await fetch('https://developer.aliyun.com/developer/api/my/user/getUser', { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'application/json', - } - }) - const data = await response.json() - - if (data.success && data.data?.nickname) { - const username = data.data.nickname - const avatar = data.data.avatar || '' - console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') - return { loggedIn: true, username, avatar } - } else { - console.log(`[COSE] ${platformId} API 返回未登录状态`) - return { loggedIn: false } - } - } catch (e) { - console.log(`[COSE] ${platformId} 获取用户信息失败:`, e.message) - return { loggedIn: false } - } - } - - // 华为云开发者博客特殊处理 - if (platformId === 'huaweicloud') { - try { - // 查找华为云相关页面(优先 bbs,其次其他华为云页面) - let tabs = await chrome.tabs.query({ url: 'https://bbs.huaweicloud.com/*' }) - - if (tabs.length === 0) { - tabs = await chrome.tabs.query({ url: 'https://*.huaweicloud.com/*' }) - } - - if (tabs.length === 0) { - console.log(`[COSE] ${platformId} 没有打开的华为云页面`) - return { loggedIn: false } - } - - console.log(`[COSE] ${platformId} 使用页面:`, tabs[0].url) - - // 在页面上下文中调用 API(使用同步包装) - try { - const results = await chrome.scripting.executeScript({ - target: { tabId: tabs[0].id }, - func: () => { - return new Promise((resolve) => { - const csrf = document.cookie.match(/csrf=([^;]+)/)?.[1] || '' - fetch('https://devdata.huaweicloud.com/rest/developer/fwdu/rest/developer/user/hdcommunityservice/v1/member/get-personal-info', { - method: 'GET', - credentials: 'include', - headers: { 'Accept': 'application/json', 'csrf': csrf } - }) - .then(response => response.ok ? response.json() : null) - .then(data => { - if (data && data.memName) { - resolve({ memName: data.memName, memAlias: data.memAlias, memPhoto: data.memPhoto }) - } else { - resolve(null) - } - }) - .catch(() => resolve(null)) - }) - } - }) - - // executeScript 返回的 result 可能是 Promise,需要 await - let data = results?.[0]?.result - if (data && typeof data.then === 'function') { - data = await data - } - - if (data && data.memName) { - console.log(`[COSE] ${platformId} 已登录:`, data.memName) - return { - loggedIn: true, - username: data.memAlias || data.memName, - avatar: data.memPhoto || '' - } - } - console.log(`[COSE] ${platformId} API 返回:`, data) - } catch (e) { - console.log(`[COSE] ${platformId} scripting 失败:`, e.message) - } - - return { loggedIn: false } - } catch (e) { - console.log(`[COSE] ${platformId} 检测失败:`, e.message) - return { loggedIn: false } - } - } - - // 华为开发者文章特殊处理 - if (platformId === 'huaweidev') { - try { - // 检查 developer_userinfo cookie - const userInfoCookie = await chrome.cookies.get({ - url: 'https://developer.huawei.com', - name: 'developer_userinfo' - }) - - if (!userInfoCookie || !userInfoCookie.value) { - console.log(`[COSE] ${platformId} 未找到 developer_userinfo cookie,未登录`) - return { loggedIn: false } - } - - console.log(`[COSE] ${platformId} 找到 developer_userinfo cookie`) - - // 解析 cookie 获取 csrfToken - let csrfToken = '' - try { - const userInfoData = JSON.parse(decodeURIComponent(userInfoCookie.value)) - csrfToken = userInfoData.csrftoken || '' - } catch (e) { - console.log(`[COSE] ${platformId} 解析 cookie 失败:`, e.message) - } - - // 如果没有 csrfToken,尝试从单独的 cookie 获取 - if (!csrfToken) { - const csrfCookie = await chrome.cookies.get({ - url: 'https://developer.huawei.com', - name: 'csrfToken' - }) - csrfToken = csrfCookie?.value || '' - } - - if (!csrfToken) { - console.log(`[COSE] ${platformId} 未找到 csrfToken,返回已登录但无用户信息`) - return { loggedIn: true, username: '', avatar: '' } - } - - // 通过 API 获取用户信息 - const now = new Date() - const hdDate = now.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '') - const serialNo = Math.floor(Math.random() * 10000000).toString() - - const response = await fetch('https://svc-drcn.developer.huawei.com/codeserver/Common/v1/delegate', { - method: 'POST', - credentials: 'include', - headers: { - 'Accept': 'application/json, text/plain, */*', - 'Content-Type': 'application/json;charset=UTF-8', - 'x-hd-csrf': csrfToken, - 'x-hd-date': hdDate, - 'x-hd-serialno': serialNo, - 'Origin': 'https://developer.huawei.com', - 'Referer': 'https://developer.huawei.com/' - }, - body: JSON.stringify({ - svc: 'GOpen.User.getInfo', - reqType: 0, - reqJson: JSON.stringify({ getNickName: '1' }) - }) - }) - - const data = await response.json() - - if (data.returnCode === '0' && data.resJson) { - const userInfo = JSON.parse(data.resJson) - const username = userInfo.displayName || userInfo.loginID || '' - const avatar = userInfo.headPictureURL || '' - console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') - return { loggedIn: true, username, avatar } - } else { - console.log(`[COSE] ${platformId} API 返回错误:`, data.returnCode, data.description) - return { loggedIn: true, username: '', avatar: '' } - } - } catch (e) { - console.log(`[COSE] ${platformId} 检测失败:`, e.message) - return { loggedIn: false } - } - } - - // 少数派特殊处理:通过 /api/v1/user/info/get API 获取用户信息 - // 需要从 cookie 中读取 JWT token 并添加到 Authorization header - if (platformId === 'sspai') { - try { - // 先获取 JWT token - const jwtCookie = await chrome.cookies.get({ - url: 'https://sspai.com', - name: 'sspai_jwt_token' - }) - - if (!jwtCookie || !jwtCookie.value) { - console.log(`[COSE] ${platformId} 未找到 JWT token`) - return { loggedIn: false } - } - - const token = jwtCookie.value - const response = await fetch('https://sspai.com/api/v1/user/info/get', { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'application/json', - 'Authorization': `Bearer ${token}`, - } - }) - const data = await response.json() - - if (data.error === 0 && data.data?.nickname) { - username = data.data.nickname - // 少数派 CDN 没有跨域限制,直接使用原始头像 URL - avatar = data.data.avatar || '' - console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') - return { loggedIn: true, username, avatar } - } else { - console.log(`[COSE] ${platformId} API 返回未登录状态:`, data.msg || data.error) - return { loggedIn: false } - } - } catch (e) { - console.log(`[COSE] ${platformId} 获取用户信息失败:`, e.message) - return { loggedIn: false } - } - } - - // 腾讯云开发者社区特殊处理:通过创作中心页面获取用户信息 - if (platformId === 'tencentcloud') { - try { - const response = await fetch('https://cloud.tencent.com/developer/creator', { - method: 'GET', - credentials: 'include', - }) - const html = await response.text() - const finalUrl = response.url - - // 检查是否被重定向到首页(未登录时会重定向) - if (!finalUrl.includes('/creator')) { - console.log(`[COSE] ${platformId} 未登录:被重定向到 ${finalUrl}`) - return { loggedIn: false } - } - - // 检查页面是否包含登录按钮(未登录标志) - if (html.includes('登录/注册') || html.includes('"isLogin":false') || html.includes('"login":false')) { - console.log(`[COSE] ${platformId} 未登录:页面包含登录按钮`) - return { loggedIn: false } - } - - // 从创作中心页面提取当前用户信息(通常在页面的用户信息区域) - // 匹配 "userInfo" 或 "creatorInfo" 等包含当前用户信息的 JSON 对象 - const userInfoMatch = html.match(/"userInfo"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) || - html.match(/"creatorInfo"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) || - html.match(/"currentUser"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) - - // 备用方案:匹配创作中心特有的用户信息结构 - const creatorNicknameMatch = html.match(/class="creator-info[^"]*"[^>]*>[\s\S]*?<[^>]*class="[^"]*name[^"]*"[^>]*>([^<]+) - const shortName = username.replace(/\d+$/, '') // 移除末尾数字,如 fwen925 -> fwen - const avatarPattern = new RegExp(`]*alt="${shortName}"[^>]*src="([^"]+)"`) - const avatarMatch = html.match(avatarPattern) - - // 备用: src 在 alt 之前 - const avatarPattern2 = new RegExp(`]*src="([^"]+)"[^>]*alt="${shortName}"`) - const avatarMatch2 = html.match(avatarPattern2) - - avatar = (avatarMatch && avatarMatch[1]) || (avatarMatch2 && avatarMatch2[1]) || '' - - console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') - return { loggedIn: true, username, avatar } - } else { - // 有 cookie 但无法提取用户名,仍然认为已登录 - console.log(`[COSE] ${platformId} 已登录但无法提取用户名`) - return { loggedIn: true, username: '', avatar: '' } - } - } catch (e) { - console.log(`[COSE] ${platformId} 获取用户信息失败:`, e.message) - return { loggedIn: false } - } - } - - // Twitter/X 特殊处理:检查 auth_token 和 ct0 cookies - if (platformId === 'twitter') { - try { - // Twitter 使用 auth_token 和 ct0 cookies 来标识登录状态 - const authTokenCookie = await chrome.cookies.get({ - url: 'https://x.com', - name: 'auth_token' - }) - const ct0Cookie = await chrome.cookies.get({ - url: 'https://x.com', - name: 'ct0' - }) - - // 如果没有 auth_token cookie,说明未登录 - if (!authTokenCookie) { - console.log(`[COSE] ${platformId} 未找到 auth_token cookie,未登录`) - return { loggedIn: false } - } - - console.log(`[COSE] ${platformId} 找到登录 cookie: auth_token=${!!authTokenCookie}, ct0=${!!ct0Cookie}`) - - // 尝试获取用户信息 - let username = '' - let avatar = '' - - try { - // 方法1: 从 Twitter 首页 HTML 中提取用户信息 - const response = await fetch('https://x.com/home', { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'text/html', - } - }) - - if (response.ok) { - const html = await response.text() - - // 从页面脚本中提取 screen_name - const screenNameMatch = html.match(/"screen_name"\s*:\s*"([^"]+)"/) - if (screenNameMatch) { - username = screenNameMatch[1] - } - - // 从页面脚本中提取头像 URL - const avatarMatch = html.match(/"profile_image_url_https"\s*:\s*"([^"]+)"/) - if (avatarMatch) { - // 将 _normal 替换为 _x96 获取更大的头像 - avatar = avatarMatch[1].replace('_normal.', '_x96.') - } - - console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') - } - } catch (e) { - console.log(`[COSE] ${platformId} 获取用户信息失败:`, e.message) - } - - // 有 auth_token cookie 就认为已登录 - return { loggedIn: true, username, avatar } - } catch (e) { - console.log(`[COSE] ${platformId} 检测失败:`, e.message) - return { loggedIn: false } - } - } - - // 百度云千帆特殊处理:通过 API 获取用户信息 - if (platformId === 'qianfan') { - try { - // 调用千帆社区用户信息 API - const response = await fetch('https://qianfan.cloud.baidu.com/api/community/user/current', { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'application/json', - } - }) - - if (!response.ok) { - console.log(`[COSE] ${platformId} API 请求失败: ${response.status}`) - return { loggedIn: false } - } - - const data = await response.json() - console.log(`[COSE] ${platformId} API 响应:`, data) - - // 检查 API 返回是否成功且有用户信息 - if (data.success && data.result) { - const username = data.result.displayName || data.result.nickname || '' - const avatar = data.result.avatar || '' - - console.log(`[COSE] ${platformId} 用户信息: ${username}, ${avatar ? '有头像' : '无头像'}`) - return { loggedIn: true, username, avatar } - } else { - console.log(`[COSE] ${platformId} 未登录或无用户信息`) - return { loggedIn: false } - } - } catch (e) { - console.log(`[COSE] ${platformId} 检测失败:`, e.message) - return { loggedIn: false } - } - } - - // 从页面抓取用户信息 - if (config.fetchUserInfoFromPage && config.userInfoUrl) { - try { - const response = await fetch(config.userInfoUrl, { - method: 'GET', - credentials: 'include' - }) - const html = await response.text() - - // 头条号用户信息提取 - if (platformId === 'toutiao') { - // 尝试从页面中提取用户名 - const nameMatch = html.match(/\"name\"\s*:\s*\"([^"]+)\"/i) || - html.match(/screen_name[\"']?\s*[:=]\s*[\"']([^\"']+)[\"']/i) || - html.match(/]*class="[^"]*name[^"]*"[^>]*>([^<]+)<\/span>/i) - if (nameMatch) { - username = nameMatch[1] - } - // 尝试从页面中提取头像 - const avatarMatch = html.match(/\"avatar_url\"\s*:\s*\"([^"]+)\"/i) || - html.match(/avatar[\"']?\s*[:=]\s*[\"']([^\"']+)[\"']/i) - if (avatarMatch) { - avatar = avatarMatch[1].replace(/\\/g, '') - } - console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') - } - // 思否用户信息提取(从 __NEXT_DATA__ 中获取) - // 注意:思否的 PHPSESSID 即使未登录也存在,必须通过用户信息判断登录状态 - else if (platformId === 'segmentfault') { - // 从 __NEXT_DATA__ 中提取用户信息 - const nextDataMatch = html.match(/]*>([^<]+)<\/script>/i) - if (nextDataMatch) { - try { - const nextData = JSON.parse(nextDataMatch[1]) - const sessionUser = nextData?.props?.pageProps?.initialState?.global?.sessionUser?.user - if (sessionUser && sessionUser.name) { - username = sessionUser.name - avatar = sessionUser.avatar_url || '' - } - } catch (e) { - console.log(`[COSE] ${platformId} 解析 NEXT_DATA 失败:`, e.message) - } - } - // 思否:只有成功提取到用户名才算已登录 - if (!username) { - console.log(`[COSE] ${platformId} 未登录或无法获取用户信息`) - return { loggedIn: false } - } - console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') - } - // 微信公众号用户信息提取 - else if (platformId === 'wechat') { - // 从 HTML 中提取公众号名称 - const nameMatch = html.match(/nick_name\s*[:=]\s*["']([^"']+)["']/i) || - html.match(/]*class="nickname"[^>]*>([^<]+)<\/span>/i) - if (nameMatch) { - username = nameMatch[1] - } - // 从 HTML 中提取头像 - const avatarMatch = html.match(/head_img\s*[:=]\s*["']([^"']+)["']/i) || - html.match(/]*class="avatar"[^>]*src="([^"]+)"/i) - if (avatarMatch) { - avatar = avatarMatch[1].replace(/\\x26amp;/g, '&').replace(/\\/g, '') - if (!avatar.startsWith('http')) { - avatar = 'https://mp.weixin.qq.com' + avatar - } - } - console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') - } - // OSChina 用户信息提取 - else if (platformId === 'oschina') { - // 提取用户 ID(从个人空间链接) - const uidMatch = html.match(/href=["']https:\/\/my\.oschina\.net\/u\/(\d+)["']/i) || - html.match(/space\.oschina\.net\/u\/(\d+)/i) || - html.match(/data-user-id=["'](\d+)["']/i) - - let userId = null - if (uidMatch) { - userId = uidMatch[1] - PLATFORM_USER_INFO['oschina'] = { userId } - console.log(`[COSE] ${platformId} 获取到 userId:`, userId) - } - - // 从 HTML 中提取用户名 - const nameMatch = html.match(/]*class="[^"]*user-name[^"]*"[^>]*>([^<]+)<\/a>/i) || - html.match(/]*class="[^"]*nick[^"]*"[^>]*>([^<]+)<\/span>/i) || - html.match(/title="([^"]+)"[^>]*class="[^"]*avatar/i) || - html.match(/class="[^"]*avatar[^"]*"[^>]*title="([^"]+)"/i) || - html.match(/alt="([^"]+)"[^>]*class="[^"]*avatar/i) - - if (nameMatch) { - username = nameMatch[1].trim() - } - - // 如果首页没提取到用户名,但有 userId,尝试从个人空间获取 - if (!username && userId) { - try { - console.log(`[COSE] ${platformId} 尝试从个人空间获取用户名...`) - const userPageResp = await fetch(`https://my.oschina.net/u/${userId}`, { - method: 'GET', - credentials: 'include' - }) - const userPageHtml = await userPageResp.text() - const userPageNameMatch = userPageHtml.match(/]*class="[^"]*header[^"]*"[^>]*>([^<]+)<\/h3>/i) || - userPageHtml.match(/]*class="[^"]*user-name[^"]*"[^>]*>([^<]+)<\/div>/i) || - userPageHtml.match(/([^<]+)的个人空间<\/title>/i) - if (userPageNameMatch) { - username = userPageNameMatch[1].trim() - console.log(`[COSE] ${platformId} 从个人空间获取用户名成功:`, username) - } - } catch (err) { - console.error(`[COSE] ${platformId} 获取个人空间失败:`, err) - } - } - - // 如果还是没有用户名,但有 userId,使用 userId 作为兜底 - if (!username && userId) { - username = userId - } - - // 头像提取 - const avatarMatch = html.match(/<img[^>]*src="([^"]+)"[^>]*class="[^"]*avatar/i) || - html.match(/<img[^>]*class="[^"]*avatar[^"]*"[^>]*src="([^"]+)"/i) - if (avatarMatch) { - avatar = avatarMatch[1] - } - - console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') - } - // 通用模块化解析逻辑 (支持 51CTO 等新平台) - else if (config.parseUserInfo) { - console.log(`[COSE] ${platformId} 使用通用解析逻辑`) - const info = config.parseUserInfo(html) - if (info) { - // 允许解析函数显式返回未登录状态 - if (info.loggedIn === false) { - console.log(`[COSE] ${platformId} 解析结果判定为未登录`) - return { loggedIn: false } - } - if (info.username) username = info.username - if (info.avatar) avatar = info.avatar - // 如果解析出了 userId,保存到全局存储 - if (info.userId && typeof PLATFORM_USER_INFO !== 'undefined') { - PLATFORM_USER_INFO[platformId] = { userId: info.userId } - console.log(`[COSE] ${platformId} 获取到 userId:`, info.userId) - } - } - console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') - } - } catch (e) { - console.log(`[COSE] ${platformId} 获取用户信息失败:`, e.message) - } - } - - console.log(`[COSE] ${platformId} 登录用户:`, username, avatar ? '有头像' : '无头像') - return { loggedIn: true, username, avatar } - } catch (error) { - console.error(`[COSE] ${platformId} cookie检测错误:`, error) - return { loggedIn: false, error: error.message } - } -} - -// 使用 Debugger API 发送真实的 Ctrl+V 粘贴 async function pasteWithDebugger(tabId) { const debuggee = { tabId } diff --git a/packages/detection/index.js b/packages/detection/index.js index 9ac5727..ee55bf3 100644 --- a/packages/detection/index.js +++ b/packages/detection/index.js @@ -1,373 +1,3 @@ -/** - * @cose/detection - Platform login detection module - * - * This package provides login detection configurations for all supported platforms. - * Each config includes: - * - api: The API endpoint to check login status - * - method: HTTP method (GET/POST) - * - checkLogin: Function to determine if user is logged in from response - * - getUserInfo: Function to extract username and avatar from response - */ - -// CSDN -export const CSDNLoginConfig = { - api: 'https://passport.csdn.net/v1/api/info', - method: 'GET', - checkLogin: (response) => response?.data?.userId, - getUserInfo: (response) => ({ - username: response?.data?.nickName, - avatar: response?.data?.avatarUrl, - }), -} - -// 掘金 -export const JuejinLoginConfig = { - api: 'https://api.juejin.cn/user_api/v1/user/get', - method: 'GET', - checkLogin: (response) => response?.err_no === 0 && response?.data?.user_id, - getUserInfo: (response) => ({ - username: response?.data?.user_name, - avatar: response?.data?.avatar_large, - }), -} - -// 微信公众号 -export const WechatLoginConfig = { - api: 'https://mp.weixin.qq.com/cgi-bin/logininfo?action=logininfo&lang=zh_CN', - method: 'GET', - checkLogin: (response) => response?.base_resp?.ret === 0 && response?.user_name, - getUserInfo: (response) => ({ - username: response?.nick_name || response?.user_name, - avatar: response?.head_img, - }), -} - -// 知乎 -export const ZhihuLoginConfig = { - api: 'https://www.zhihu.com/api/v4/me', - method: 'GET', - checkLogin: (response) => response?.id, - getUserInfo: (response) => ({ - username: response?.name, - avatar: response?.avatar_url, - }), -} - -// 头条号 -export const ToutiaoLoginConfig = { - api: 'https://mp.toutiao.com/mp/agw/creator_center/user_info?app_id=1231', - method: 'GET', - checkLogin: (response) => response?.code === 0 && response?.name, - getUserInfo: (response) => ({ - username: response?.name, - avatar: response?.avatar_url, - }), -} - -// SegmentFault -export const SegmentFaultLoginConfig = { - api: 'https://segmentfault.com/gateway/user/me', - method: 'GET', - checkLogin: (response) => response?.status === 0 && response?.data?.id, - getUserInfo: (response) => ({ - username: response?.data?.name, - avatar: response?.data?.avatar_url, - }), -} - -// 博客园 -export const CnblogsLoginConfig = { - api: 'https://www.cnblogs.com/api/users/current', - method: 'GET', - checkLogin: (response) => response?.UserId, - getUserInfo: (response) => ({ - username: response?.DisplayName, - avatar: response?.Avatar, - }), -} - -// 开源中国 -export const OSChinaLoginConfig = { - api: 'https://www.oschina.net/action/user/detail?format=json', - method: 'GET', - checkLogin: (response) => response?.id, - getUserInfo: (response) => ({ - username: response?.name, - avatar: response?.portrait, - }), -} - -// 51CTO -export const CTO51LoginConfig = { - api: 'https://home.51cto.com/api/user/info/getUserBasicInfo', - method: 'GET', - checkLogin: (response) => response?.code === 200 && response?.data?.id, - getUserInfo: (response) => ({ - username: response?.data?.nickname, - avatar: response?.data?.headpic, - }), -} - -// InfoQ -export const InfoQLoginConfig = { - api: 'https://www.infoq.cn/public/v1/my/menu', - method: 'POST', - body: JSON.stringify({}), - headers: { 'Content-Type': 'application/json' }, - checkLogin: (response) => response?.code === 0 && response?.data?.username, - getUserInfo: (response) => ({ - username: response?.data?.nickname, - avatar: response?.data?.avatar, - }), -} - -// 简书 -export const JianshuLoginConfig = { - api: 'https://www.jianshu.com/settings/basic', - method: 'GET', - isHtml: true, - checkLogin: (html) => !html.includes('登录'), - getUserInfo: () => ({ username: null, avatar: null }), -} - -// 百家号 -export const BaijiahaoLoginConfig = { - api: 'https://baijiahao.baidu.com/builder/app/appinfo', - method: 'GET', - checkLogin: (response) => response?.errno === 0 && response?.data?.user?.name, - getUserInfo: (response) => ({ - username: response?.data?.user?.name, - avatar: response?.data?.user?.avatar, - }), -} - -// 网易号 -export const WangyihaoLoginConfig = { - api: 'https://mp.163.com/api/account/info', - method: 'GET', - checkLogin: (response) => response?.code === 1000 && response?.data?.nickname, - getUserInfo: (response) => ({ - username: response?.data?.nickname, - avatar: response?.data?.headImg, - }), -} - -// 腾讯云开发者社区 -export const TencentCloudLoginConfig = { - api: 'https://cloud.tencent.com/developer/api/user/session', - method: 'GET', - checkLogin: (response) => response?.code === 0 && response?.data?.uin, - getUserInfo: (response) => ({ - username: response?.data?.nickname, - avatar: response?.data?.avatar, - }), -} - -// Medium -export const MediumLoginConfig = { - api: 'https://medium.com/me/stats?format=json', - method: 'GET', - checkLogin: (response) => response?.success, - getUserInfo: (response) => ({ - username: response?.payload?.user?.name, - avatar: response?.payload?.user?.imageId ? `https://miro.medium.com/v2/resize:fill:64:64/${response.payload.user.imageId}` : null, - }), -} - -// 少数派 -export const SspaiLoginConfig = { - api: 'https://sspai.com/api/v1/user/info/get', - method: 'GET', - checkLogin: (response) => response?.error === 0 && response?.data?.nickname, - getUserInfo: (response) => ({ - username: response?.data?.nickname, - avatar: response?.data?.avatar, - }), -} - -// 搜狐号 -export const SohuLoginConfig = { - api: 'https://mp.sohu.com/mpbp/bp/account/list', - method: 'GET', - checkLogin: (response) => response?.success === true && response?.data?.total > 0, - getUserInfo: (response) => { - const account = response?.data?.data?.[0]?.accounts?.[0] - let avatar = account?.avatar || '' - if (avatar && avatar.startsWith('//')) { - avatar = 'https:' + avatar - } - return { - username: account?.nickName, - avatar: avatar, - } - }, -} - -// B站 -export const BilibiliLoginConfig = { - api: 'https://api.bilibili.com/x/web-interface/nav', - method: 'GET', - checkLogin: (response) => response?.code === 0 && response?.data?.isLogin === true, - getUserInfo: (response) => ({ - username: response?.data?.uname, - avatar: response?.data?.face, - }), -} - -// 微博 -export const WeiboLoginConfig = { - api: 'https://card.weibo.com/article/v3/aj/editor/draft/list?page=1&pagesize=1', - method: 'GET', - checkLogin: (response) => response?.code === 100000, - getUserInfo: () => ({ username: null, avatar: null }), -} - -// 阿里云开发者社区 -export const AliyunLoginConfig = { - api: 'https://developer.aliyun.com/developer/api/my/user/getUser', - method: 'GET', - checkLogin: (response) => response?.success && response?.data?.accountId, - getUserInfo: (response) => ({ - username: response?.data?.nick, - avatar: response?.data?.avatar, - }), -} - -// 华为云开发者博客 -export const HuaweiCloudLoginConfig = { - api: 'https://bbs.huaweicloud.com/uucenter/user/getUserInfoByUserNos', - method: 'GET', - checkLogin: (response) => response?.result === 'success', - getUserInfo: () => ({ username: null, avatar: null }), -} - -// 华为开发者联盟 -export const HuaweiDevLoginConfig = { - api: 'https://developer.huawei.com/consumer/cn/doc/distribution/dev-web/overview-web-0000001049579028', - method: 'GET', - isHtml: true, - checkLogin: (html) => html.includes('logout'), - getUserInfo: () => ({ username: null, avatar: null }), -} - -// Twitter/X -export const TwitterLoginConfig = { - api: 'https://api.x.com/1.1/account/settings.json', - method: 'GET', - checkLogin: (response) => response?.screen_name, - getUserInfo: (response) => ({ - username: response?.screen_name, - avatar: null, - }), -} - -// 百度千帆 -export const QianfanLoginConfig = { - api: 'https://qianfan.cloud.baidu.com/api/developer/common/userInfo', - method: 'GET', - checkLogin: (response) => response?.code === 200 && response?.data?.userName, - getUserInfo: (response) => ({ - username: response?.data?.userName, - avatar: null, - }), -} - -// 支付宝开放平台 -export const AlipayOpenLoginConfig = { - api: 'https://open.alipay.com/api/user/getUserInfo', - method: 'GET', - checkLogin: (response) => response?.data?.loginId, - getUserInfo: (response) => ({ - username: response?.data?.loginId, - avatar: response?.data?.avatar, - }), -} - -// ModelScope -export const ModelScopeLoginConfig = { - api: 'https://modelscope.cn/api/v1/user/current', - method: 'GET', - checkLogin: (response) => response?.Success && response?.Data?.Name, - getUserInfo: (response) => ({ - username: response?.Data?.Name, - avatar: response?.Data?.Avatar, - }), -} - -// 火山引擎 -export const VolcengineLoginConfig = { - api: 'https://developer.volcengine.com/api/console/user/info', - method: 'GET', - checkLogin: (response) => response?.code === 0 && response?.data?.display_name, - getUserInfo: (response) => ({ - username: response?.data?.display_name, - avatar: null, - }), -} - -// 抖音 -export const DouyinLoginConfig = { - api: 'https://creator.douyin.com/web/api/media/user/info/', - method: 'GET', - checkLogin: (response) => response?.status_code === 0 && (response?.user?.uid || response?.user_info?.uid), - getUserInfo: (response) => ({ - username: response?.user?.nickname || response?.user_info?.nickname, - avatar: (response?.user?.avatar_thumb?.url_list?.[0] || response?.user_info?.avatar_thumb?.url_list?.[0]), - }), -} - -// 小红书 -export const XiaohongshuLoginConfig = { - api: 'https://creator.xiaohongshu.com/api/galaxy/user/index', - method: 'GET', - checkLogin: (response) => response?.success && response?.data?.id, - getUserInfo: (response) => ({ - username: response?.data?.nickname, - avatar: response?.data?.portrait, - }), -} - -// 电子发烧友 -export const ElecfansLoginConfig = { - api: 'https://bbs.elecfans.com/api/login/check', - method: 'GET', - checkLogin: (response) => response?.status === 1, - getUserInfo: (response) => ({ - username: response?.data?.username, - avatar: response?.data?.avatar, - }), -} - -// 统一的 LOGIN_CHECK_CONFIG 对象(按平台 ID 索引) -export const LOGIN_CHECK_CONFIG = { - csdn: CSDNLoginConfig, - juejin: JuejinLoginConfig, - wechat: WechatLoginConfig, - zhihu: ZhihuLoginConfig, - toutiao: ToutiaoLoginConfig, - segmentfault: SegmentFaultLoginConfig, - cnblogs: CnblogsLoginConfig, - oschina: OSChinaLoginConfig, - cto51: CTO51LoginConfig, - infoq: InfoQLoginConfig, - jianshu: JianshuLoginConfig, - baijiahao: BaijiahaoLoginConfig, - wangyihao: WangyihaoLoginConfig, - tencentcloud: TencentCloudLoginConfig, - medium: MediumLoginConfig, - sspai: SspaiLoginConfig, - sohu: SohuLoginConfig, - bilibili: BilibiliLoginConfig, - weibo: WeiboLoginConfig, - aliyun: AliyunLoginConfig, - huaweicloud: HuaweiCloudLoginConfig, - huaweidev: HuaweiDevLoginConfig, - twitter: TwitterLoginConfig, - qianfan: QianfanLoginConfig, - alipayopen: AlipayOpenLoginConfig, - modelscope: ModelScopeLoginConfig, - volcengine: VolcengineLoginConfig, - douyin: DouyinLoginConfig, - xiaohongshu: XiaohongshuLoginConfig, - elecfans: ElecfansLoginConfig, -} +export * from './src/configs.js' +export * from './src/detect.js' +export * from './src/utils.js' diff --git a/packages/detection/src/configs.js b/packages/detection/src/configs.js new file mode 100644 index 0000000..3c8d27e --- /dev/null +++ b/packages/detection/src/configs.js @@ -0,0 +1,361 @@ +/** + * @cose/detection - Platform login detection module + * + * This package provides login detection configurations for all supported platforms. + * Each config includes: + * - api: The API endpoint to check login status + * - method: HTTP method (GET/POST) + * - checkLogin: Function to determine if user is logged in from response + * - getUserInfo: Function to extract username and avatar from response + */ + +// 掘金 +export const JuejinLoginConfig = { + api: 'https://api.juejin.cn/user_api/v1/user/get', + method: 'GET', + checkLogin: (response) => response?.err_no === 0 && response?.data?.user_id, + getUserInfo: (response) => ({ + username: response?.data?.user_name, + avatar: response?.data?.avatar_large, + }), +} + +// 微信公众号 +export const WechatLoginConfig = { + api: 'https://mp.weixin.qq.com/cgi-bin/logininfo?action=logininfo&lang=zh_CN', + method: 'GET', + checkLogin: (response) => response?.base_resp?.ret === 0 && response?.user_name, + getUserInfo: (response) => ({ + username: response?.nick_name || response?.user_name, + avatar: response?.head_img, + }), +} + +// 知乎 +export const ZhihuLoginConfig = { + api: 'https://www.zhihu.com/api/v4/me', + method: 'GET', + checkLogin: (response) => response?.id, + getUserInfo: (response) => ({ + username: response?.name, + avatar: response?.avatar_url, + }), +} + +// 头条号 +export const ToutiaoLoginConfig = { + api: 'https://mp.toutiao.com/mp/agw/creator_center/user_info?app_id=1231', + method: 'GET', + checkLogin: (response) => response?.code === 0 && response?.name, + getUserInfo: (response) => ({ + username: response?.name, + avatar: response?.avatar_url, + }), +} + +// SegmentFault +export const SegmentFaultLoginConfig = { + api: 'https://segmentfault.com/gateway/user/me', + method: 'GET', + checkLogin: (response) => response?.status === 0 && response?.data?.id, + getUserInfo: (response) => ({ + username: response?.data?.name, + avatar: response?.data?.avatar_url, + }), +} + +// 博客园 +export const CnblogsLoginConfig = { + api: 'https://www.cnblogs.com/api/users/current', + method: 'GET', + checkLogin: (response) => response?.UserId, + getUserInfo: (response) => ({ + username: response?.DisplayName, + avatar: response?.Avatar, + }), +} + +// 开源中国 +export const OSChinaLoginConfig = { + api: 'https://www.oschina.net/action/user/detail?format=json', + method: 'GET', + checkLogin: (response) => response?.id, + getUserInfo: (response) => ({ + username: response?.name, + avatar: response?.portrait, + }), +} + +// 51CTO +export const CTO51LoginConfig = { + api: 'https://home.51cto.com/api/user/info/getUserBasicInfo', + method: 'GET', + checkLogin: (response) => response?.code === 200 && response?.data?.id, + getUserInfo: (response) => ({ + username: response?.data?.nickname, + avatar: response?.data?.headpic, + }), +} + +// InfoQ +export const InfoQLoginConfig = { + api: 'https://www.infoq.cn/public/v1/my/menu', + method: 'POST', + body: JSON.stringify({}), + headers: { 'Content-Type': 'application/json' }, + checkLogin: (response) => response?.code === 0 && response?.data?.username, + getUserInfo: (response) => ({ + username: response?.data?.nickname, + avatar: response?.data?.avatar, + }), +} + +// 简书 +export const JianshuLoginConfig = { + api: 'https://www.jianshu.com/settings/basic', + method: 'GET', + isHtml: true, + checkLogin: (html) => !html.includes('登录'), + getUserInfo: () => ({ username: null, avatar: null }), +} + +// 百家号 +export const BaijiahaoLoginConfig = { + api: 'https://baijiahao.baidu.com/builder/app/appinfo', + method: 'GET', + checkLogin: (response) => response?.errno === 0 && response?.data?.user?.name, + getUserInfo: (response) => ({ + username: response?.data?.user?.name, + avatar: response?.data?.user?.avatar, + }), +} + +// 网易号 +export const WangyihaoLoginConfig = { + api: 'https://mp.163.com/api/account/info', + method: 'GET', + checkLogin: (response) => response?.code === 1000 && response?.data?.nickname, + getUserInfo: (response) => ({ + username: response?.data?.nickname, + avatar: response?.data?.headImg, + }), +} + +// 腾讯云开发者社区 +export const TencentCloudLoginConfig = { + api: 'https://cloud.tencent.com/developer/api/user/session', + method: 'GET', + checkLogin: (response) => response?.code === 0 && response?.data?.uin, + getUserInfo: (response) => ({ + username: response?.data?.nickname, + avatar: response?.data?.avatar, + }), +} + +// Medium +export const MediumLoginConfig = { + api: 'https://medium.com/me/stats?format=json', + method: 'GET', + checkLogin: (response) => response?.success, + getUserInfo: (response) => ({ + username: response?.payload?.user?.name, + avatar: response?.payload?.user?.imageId ? `https://miro.medium.com/v2/resize:fill:64:64/${response.payload.user.imageId}` : null, + }), +} + +// 少数派 +export const SspaiLoginConfig = { + api: 'https://sspai.com/api/v1/user/info/get', + method: 'GET', + checkLogin: (response) => response?.error === 0 && response?.data?.nickname, + getUserInfo: (response) => ({ + username: response?.data?.nickname, + avatar: response?.data?.avatar, + }), +} + +// 搜狐号 +export const SohuLoginConfig = { + api: 'https://mp.sohu.com/mpbp/bp/account/list', + method: 'GET', + checkLogin: (response) => response?.success === true && response?.data?.total > 0, + getUserInfo: (response) => { + const account = response?.data?.data?.[0]?.accounts?.[0] + let avatar = account?.avatar || '' + if (avatar && avatar.startsWith('//')) { + avatar = 'https:' + avatar + } + return { + username: account?.nickName, + avatar: avatar, + } + }, +} + +// B站 +export const BilibiliLoginConfig = { + api: 'https://api.bilibili.com/x/web-interface/nav', + method: 'GET', + checkLogin: (response) => response?.code === 0 && response?.data?.isLogin === true, + getUserInfo: (response) => ({ + username: response?.data?.uname, + avatar: response?.data?.face, + }), +} + +// 微博 +export const WeiboLoginConfig = { + api: 'https://card.weibo.com/article/v3/aj/editor/draft/list?page=1&pagesize=1', + method: 'GET', + checkLogin: (response) => response?.code === 100000, + getUserInfo: () => ({ username: null, avatar: null }), +} + +// 阿里云开发者社区 +export const AliyunLoginConfig = { + api: 'https://developer.aliyun.com/developer/api/my/user/getUser', + method: 'GET', + checkLogin: (response) => response?.success && response?.data?.accountId, + getUserInfo: (response) => ({ + username: response?.data?.nick, + avatar: response?.data?.avatar, + }), +} + +// 华为云开发者博客 +export const HuaweiCloudLoginConfig = { + api: 'https://bbs.huaweicloud.com/uucenter/user/getUserInfoByUserNos', + method: 'GET', + checkLogin: (response) => response?.result === 'success', + getUserInfo: () => ({ username: null, avatar: null }), +} + +// 华为开发者联盟 +export const HuaweiDevLoginConfig = { + api: 'https://developer.huawei.com/consumer/cn/doc/distribution/dev-web/overview-web-0000001049579028', + method: 'GET', + isHtml: true, + checkLogin: (html) => html.includes('logout'), + getUserInfo: () => ({ username: null, avatar: null }), +} + +// Twitter/X +export const TwitterLoginConfig = { + api: 'https://api.x.com/1.1/account/settings.json', + method: 'GET', + checkLogin: (response) => response?.screen_name, + getUserInfo: (response) => ({ + username: response?.screen_name, + avatar: null, + }), +} + +// 百度千帆 +export const QianfanLoginConfig = { + api: 'https://qianfan.cloud.baidu.com/api/developer/common/userInfo', + method: 'GET', + checkLogin: (response) => response?.code === 200 && response?.data?.userName, + getUserInfo: (response) => ({ + username: response?.data?.userName, + avatar: null, + }), +} + +// 支付宝开放平台 +export const AlipayOpenLoginConfig = { + api: 'https://open.alipay.com/api/user/getUserInfo', + method: 'GET', + checkLogin: (response) => response?.data?.loginId, + getUserInfo: (response) => ({ + username: response?.data?.loginId, + avatar: response?.data?.avatar, + }), +} + +// ModelScope +export const ModelScopeLoginConfig = { + api: 'https://modelscope.cn/api/v1/user/current', + method: 'GET', + checkLogin: (response) => response?.Success && response?.Data?.Name, + getUserInfo: (response) => ({ + username: response?.Data?.Name, + avatar: response?.Data?.Avatar, + }), +} + +// 火山引擎 +export const VolcengineLoginConfig = { + api: 'https://developer.volcengine.com/api/console/user/info', + method: 'GET', + checkLogin: (response) => response?.code === 0 && response?.data?.display_name, + getUserInfo: (response) => ({ + username: response?.data?.display_name, + avatar: null, + }), +} + +// 抖音 +export const DouyinLoginConfig = { + api: 'https://creator.douyin.com/web/api/media/user/info/', + method: 'GET', + checkLogin: (response) => response?.status_code === 0 && (response?.user?.uid || response?.user_info?.uid), + getUserInfo: (response) => ({ + username: response?.user?.nickname || response?.user_info?.nickname, + avatar: (response?.user?.avatar_thumb?.url_list?.[0] || response?.user_info?.avatar_thumb?.url_list?.[0]), + }), +} + +// 小红书 +export const XiaohongshuLoginConfig = { + api: 'https://creator.xiaohongshu.com/api/galaxy/user/index', + method: 'GET', + checkLogin: (response) => response?.success && response?.data?.id, + getUserInfo: (response) => ({ + username: response?.data?.nickname, + avatar: response?.data?.portrait, + }), +} + +// 电子发烧友 +export const ElecfansLoginConfig = { + api: 'https://bbs.elecfans.com/api/login/check', + method: 'GET', + checkLogin: (response) => response?.status === 1, + getUserInfo: (response) => ({ + username: response?.data?.username, + avatar: response?.data?.avatar, + }), +} + +// 统一的 LOGIN_CHECK_CONFIG 对象(按平台 ID 索引) +export const LOGIN_CHECK_CONFIG = { + juejin: JuejinLoginConfig, + wechat: WechatLoginConfig, + zhihu: ZhihuLoginConfig, + toutiao: ToutiaoLoginConfig, + segmentfault: SegmentFaultLoginConfig, + cnblogs: CnblogsLoginConfig, + oschina: OSChinaLoginConfig, + cto51: CTO51LoginConfig, + infoq: InfoQLoginConfig, + jianshu: JianshuLoginConfig, + baijiahao: BaijiahaoLoginConfig, + wangyihao: WangyihaoLoginConfig, + tencentcloud: TencentCloudLoginConfig, + medium: MediumLoginConfig, + sspai: SspaiLoginConfig, + sohu: SohuLoginConfig, + bilibili: BilibiliLoginConfig, + weibo: WeiboLoginConfig, + aliyun: AliyunLoginConfig, + huaweicloud: HuaweiCloudLoginConfig, + huaweidev: HuaweiDevLoginConfig, + twitter: TwitterLoginConfig, + qianfan: QianfanLoginConfig, + alipayopen: AlipayOpenLoginConfig, + modelscope: ModelScopeLoginConfig, + volcengine: VolcengineLoginConfig, + douyin: DouyinLoginConfig, + xiaohongshu: XiaohongshuLoginConfig, + elecfans: ElecfansLoginConfig, +} diff --git a/packages/detection/src/detect.js b/packages/detection/src/detect.js new file mode 100644 index 0000000..60b7365 --- /dev/null +++ b/packages/detection/src/detect.js @@ -0,0 +1,52 @@ +import { LOGIN_CHECK_CONFIG } from './configs.js' +import { checkLoginByCookie, detectByApi } from './utils.js' +import { detectCSDNUser } from './platforms/csdn.js' +import { detectOSChinaUser } from './platforms/oschina.js' +import * as specialDetectors from './detectors.js' + +export async function detectUser(platformId) { + console.log(`[COSE] Detection: Checking ${platformId}`) + + // 1. Specialized Detectors + if (platformId === 'csdn') return detectCSDNUser() + if (platformId === 'oschina') return detectOSChinaUser() + + // Detectors from bundled file + // Convention: detect{CapitalizedPlatformId}User + // Map platformId to function name + const specialMap = { + 'alipayopen': specialDetectors.detectAlipayUser, + 'weibo': specialDetectors.detectWeiboUser, + 'wechat': specialDetectors.detectWechatUser, + 'xiaohongshu': specialDetectors.detectXiaohongshuUser, + 'elecfans': specialDetectors.detectElecfansUser, + 'huaweicloud': specialDetectors.detectHuaweiCloudUser, + 'huaweidev': specialDetectors.detectHuaweiDevUser, + 'sspai': specialDetectors.detectSspaiUser, + 'aliyun': specialDetectors.detectAliyunUser, + 'sohu': specialDetectors.detectSohuUser, + 'medium': specialDetectors.detectMediumUser, + 'tencentcloud': specialDetectors.detectTencentCloudUser, + 'qianfan': specialDetectors.detectQianfanUser, + 'twitter': specialDetectors.detectTwitterUser, + } + + if (specialMap[platformId]) { + return specialMap[platformId]() + } + + // 2. Generic Config-based Detection + const config = LOGIN_CHECK_CONFIG[platformId] + if (config) { + if (config.useCookie || (config.cookieNames && config.cookieNames.length > 0)) { + return checkLoginByCookie(platformId, config) + } + + // Default to API check if API is defined + if (config.api) { + return detectByApi(platformId, config) + } + } + + return { loggedIn: false, error: 'No detection available' } +} diff --git a/packages/detection/src/detectors.js b/packages/detection/src/detectors.js new file mode 100644 index 0000000..196da99 --- /dev/null +++ b/packages/detection/src/detectors.js @@ -0,0 +1,644 @@ +/** + * Specialized platform detectors moved from background.js + */ + +export async function detectAlipayUser() { + const platformId = 'alipayopen' + try { + // 从 storage 读取缓存的用户信息 + const stored = await chrome.storage.local.get('alipayopen_user') + const cachedUser = stored.alipayopen_user + + if (cachedUser && cachedUser.loggedIn) { + // 检查缓存是否过期(1小时) + const cacheAge = Date.now() - (cachedUser.cachedAt || 0) + const maxAge = 1 * 60 * 60 * 1000 // 1 hour + + if (cacheAge < maxAge) { + console.log(`[COSE] alipayopen 从缓存读取用户信息:`, cachedUser.username) + return { + loggedIn: true, + username: cachedUser.username || '', + avatar: cachedUser.avatar || '' + } + } else { + console.log(`[COSE] alipayopen 缓存已过期`) + // 清除过期缓存 + await chrome.storage.local.remove('alipayopen_user') + } + } + + // 尝试从已打开的支付宝页面获取用户信息并缓存 + let tabs = await chrome.tabs.query({ url: 'https://open.alipay.com/*' }) + if (tabs.length === 0) { + tabs = await chrome.tabs.query({ url: 'https://*.alipay.com/*' }) + } + + if (tabs.length > 0) { + try { + // 在已打开的页面上下文中调用 API + const results = await chrome.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: async () => { + try { + const response = await fetch('https://developerportal.alipay.com/octopus/service.do', { + method: 'POST', + credentials: 'include', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + body: 'data=%5B%7B%7D%5D&serviceName=alipay.open.developerops.forum.user.query', + }) + if (!response.ok) return null + return await response.json() + } catch (e) { + return null + } + } + }) + + const data = results?.[0]?.result + console.log(`[COSE] alipayopen API 数据:`, data) + + if (data?.stat === 'ok' && data?.data?.isLoginUser === 1) { + const username = data.data.nickname || '' + const avatar = data.data.avatar || '' + + // 缓存用户信息 + await chrome.storage.local.set({ + alipayopen_user: { + loggedIn: true, + username, + avatar, + cachedAt: Date.now() + } + }) + + console.log(`[COSE] alipayopen 用户信息:`, username, avatar ? '有头像' : '无头像') + return { loggedIn: true, username, avatar } + } + } catch (e) { + console.log(`[COSE] alipayopen 从页面获取用户信息失败:`, e.message) + } + } + + console.log(`[COSE] alipayopen 未检测到登录状态`) + return { loggedIn: false } + } catch (e) { + console.log(`[COSE] alipayopen 检测失败:`, e.message) + return { loggedIn: false, error: e.message } + } +} + +export async function detectWeiboUser() { + const platformId = 'weibo' + try { + // 检查 card.weibo.com 的 SUBP cookie + const subpCookie = await chrome.cookies.get({ + url: 'https://card.weibo.com', + name: 'SUBP' + }) + + // 也检查 ALF cookie + const alfCookie = await chrome.cookies.get({ + url: 'https://card.weibo.com', + name: 'ALF' + }) + + if (!subpCookie && !alfCookie) { + console.log(`[COSE] weibo 未找到登录 cookie,未登录`) + return { loggedIn: false } + } + + // 有 cookie,通过 fetch HTML 获取用户信息 + let username = '' + let avatar = '' + + try { + // 获取所有相关 cookies 并手动添加到请求 + const weiboCookies = await chrome.cookies.getAll({ domain: '.weibo.com' }) + const cardCookies = await chrome.cookies.getAll({ domain: 'card.weibo.com' }) + const sinaCookies = await chrome.cookies.getAll({ domain: '.sina.com.cn' }) + const allCookies = [...weiboCookies, ...cardCookies, ...sinaCookies] + const cookieString = allCookies.map(c => `${c.name}=${c.value}`).join('; ') + + const response = await fetch('https://card.weibo.com/article/v5/editor', { + method: 'GET', + headers: { + 'Cookie': cookieString, + }, + credentials: 'include', + }) + const html = await response.text() + + // 从 HTML 中提取用户名 + const nickMatch = html.match(/"nick"\s*:\s*"([^"]+)"/) + if (nickMatch) { + username = nickMatch[1] + } else { + // 深度查找 nick + const altNickMatch = html.match(/\\"nick\\"\s*:\s*\\"([^\\"]+)\\"/) + if (altNickMatch) { + username = altNickMatch[1] + } + } + + // 从 HTML 中提取头像 + const avatarMatch = html.match(/"avatar_large"\s*:\s*"([^"]+)"/) + if (avatarMatch) { + avatar = avatarMatch[1].replace(/\\/g, '') + } else { + const altAvatarMatch = html.match(/\\"avatar_large\\"\s*:\s*\\"([^\\"]+)\\"/) + if (altAvatarMatch) { + let rawAvatar = altAvatarMatch[1].replace(/\\\\\\\//g, '/') + if (rawAvatar.includes('sinaimg.cn')) { + avatar = rawAvatar.split('?')[0] + } else { + avatar = rawAvatar + } + } + } + + console.log(`[COSE] weibo 用户信息: ${username}`) + } catch (e) { + console.log(`[COSE] weibo 获取用户详情失败:`, e.message) + } + + if (!username) { + return { loggedIn: false } + } + + return { loggedIn: true, username, avatar } + } catch (e) { + console.log(`[COSE] weibo 检测失败:`, e.message) + return { loggedIn: false } + } +} + +export async function detectWechatUser() { + const platformId = 'wechat' + try { + // 先检查缓存 + const stored = await chrome.storage.local.get('wechat_user') + const cachedUser = stored.wechat_user + + if (cachedUser && cachedUser.loggedIn) { + const cacheAge = Date.now() - (cachedUser.cachedAt || 0) + const maxAge = 1 * 60 * 60 * 1000 // 1 hour + + if (cacheAge < maxAge) { + console.log(`[COSE] wechat 从缓存读取:`, cachedUser.username) + return { + loggedIn: true, + username: cachedUser.username || '', + avatar: cachedUser.avatar || '' + } + } else { + await chrome.storage.local.remove('wechat_user') + } + } + + // 优先尝试在已打开的微信公众号页面中检测 + const tabs = await chrome.tabs.query({ url: 'https://mp.weixin.qq.com/*' }) + if (tabs.length > 0) { + try { + const results = await chrome.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: () => { + const wxData = window.wx?.data + if (wxData && wxData.nick_name) { + return { + loggedIn: true, + username: wxData.nick_name || wxData.user_name || '', + avatar: wxData.head_img || '', + token: wxData.t || '' + } + } + return null + } + }) + + const result = results?.[0]?.result + if (result && result.loggedIn) { + const userInfo = { ...result, cachedAt: Date.now() } + await chrome.storage.local.set({ wechat_user: userInfo }) + return { + loggedIn: true, + username: userInfo.username || '', + avatar: userInfo.avatar || '' + } + } + } catch (e) { + console.log(`[COSE] wechat 页面脚本执行失败:`, e.message) + } + } + + // 备用方案:fetch 首页并解析 HTML + try { + const response = await fetch('https://mp.weixin.qq.com/', { + method: 'GET', + credentials: 'include', + headers: { 'Accept': 'text/html' } + }) + const html = await response.text() + + if (html.includes('请使用微信扫描') || html.includes('扫码登录')) { + return { loggedIn: false } + } + + const nickMatch = html.match(/nick_name\s*[:=]\s*["']([^"']+)["']/) + const avatarMatch = html.match(/head_img\s*[:=]\s*["']([^"']+)["']/) + + if (nickMatch) { + const username = nickMatch[1] + const avatar = avatarMatch ? avatarMatch[1] : '' + await chrome.storage.local.set({ + wechat_user: { + loggedIn: true, + username, + avatar, + cachedAt: Date.now() + } + }) + return { loggedIn: true, username, avatar } + } + } catch (e) { + console.log(`[COSE] wechat fetch 失败:`, e.message) + } + + return { loggedIn: false } + } catch (e) { + console.log(`[COSE] wechat 检测失败:`, e.message) + return { loggedIn: false } + } +} + +export async function detectXiaohongshuUser() { + const platformId = 'xiaohongshu' + try { + // 先检查缓存 + const stored = await chrome.storage.local.get('xiaohongshu_user') + const cachedUser = stored.xiaohongshu_user + + if (cachedUser && cachedUser.loggedIn) { + const cacheAge = Date.now() - (cachedUser.cachedAt || 0) + const maxAge = 7 * 24 * 60 * 60 * 1000 // 7 days + if (cacheAge < maxAge) { + console.log(`[COSE] xiaohongshu 从缓存读取:`, cachedUser.username) + return { loggedIn: true, username: cachedUser.username || '', avatar: cachedUser.avatar || '' } + } else { + await chrome.storage.local.remove('xiaohongshu_user') + } + } + + // 缓存无效,尝试在已打开的小红书页面中检测 + const tabs = await chrome.tabs.query({ url: 'https://creator.xiaohongshu.com/*' }) + if (tabs.length > 0) { + const results = await chrome.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: async () => { + try { + const response = await fetch('https://creator.xiaohongshu.com/api/galaxy/user/info', { + method: 'GET', + credentials: 'include', + headers: { 'Accept': 'application/json' } + }) + if (!response.ok) return null + const data = await response.json() + if (data?.success === true && data?.code === 0 && data?.data?.userId) { + return { + loggedIn: true, + username: data.data.userName || data.data.redId || '', + avatar: data.data.userAvatar || '', + userId: data.data.userId + } + } + return null + } catch (e) { return null } + } + }) + + const result = results?.[0]?.result + if (result && result.loggedIn) { + const userInfo = { ...result, cachedAt: Date.now() } + await chrome.storage.local.set({ xiaohongshu_user: userInfo }) + return { loggedIn: true, username: userInfo.username || '', avatar: userInfo.avatar || '' } + } + } + return { loggedIn: false } + } catch (e) { + console.log(`[COSE] xiaohongshu 检测失败:`, e.message) + return { loggedIn: false } + } +} + +export async function detectElecfansUser() { + const platformId = 'elecfans' + try { + const authCookie = await chrome.cookies.get({ url: 'https://www.elecfans.com', name: 'auth' }) + const authWwwCookie = await chrome.cookies.get({ url: 'https://www.elecfans.com', name: 'auth_www' }) + + if (!authCookie && !authWwwCookie) { + return { loggedIn: false } + } + + try { + const response = await fetch('https://www.elecfans.com/webapi/passport/checklogin?_=' + Date.now(), { + method: 'GET', + credentials: 'include', + headers: { 'Accept': 'application/json, text/javascript, */*; q=0.01' } + }) + + if (!response.ok) return { loggedIn: true, username: '', avatar: '' } + + const data = await response.json() + if (data && data.uid) { + const username = data.username || '' + const avatar = data.avatar || '' + return { loggedIn: true, username, avatar } + } else { + return { loggedIn: true, username: '', avatar: '' } + } + } catch (e) { + return { loggedIn: true, username: '', avatar: '' } + } + } catch (e) { + return { loggedIn: false } + } +} + +export async function detectHuaweiCloudUser() { + const platformId = 'huaweicloud' + try { + let tabs = await chrome.tabs.query({ url: 'https://bbs.huaweicloud.com/*' }) + if (tabs.length === 0) tabs = await chrome.tabs.query({ url: 'https://*.huaweicloud.com/*' }) + if (tabs.length === 0) return { loggedIn: false } + + try { + const results = await chrome.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: () => { + return new Promise((resolve) => { + const csrf = document.cookie.match(/csrf=([^;]+)/)?.[1] || '' + fetch('https://devdata.huaweicloud.com/rest/developer/fwdu/rest/developer/user/hdcommunityservice/v1/member/get-personal-info', { + method: 'GET', + credentials: 'include', + headers: { 'Accept': 'application/json', 'csrf': csrf } + }) + .then(response => response.ok ? response.json() : null) + .then(data => { + if (data && data.memName) { + resolve({ memName: data.memName, memAlias: data.memAlias, memPhoto: data.memPhoto }) + } else { + resolve(null) + } + }) + .catch(() => resolve(null)) + }) + } + }) + + let data = results?.[0]?.result + if (data && typeof data.then === 'function') data = await data // handle promise result if applicable + + if (data && data.memName) { + return { loggedIn: true, username: data.memAlias || data.memName, avatar: data.memPhoto || '' } + } + } catch (e) { } + + return { loggedIn: false } + } catch (e) { + return { loggedIn: false } + } +} + +export async function detectHuaweiDevUser() { + const platformId = 'huaweidev' + try { + const userInfoCookie = await chrome.cookies.get({ url: 'https://developer.huawei.com', name: 'developer_userinfo' }) + if (!userInfoCookie || !userInfoCookie.value) return { loggedIn: false } + + let csrfToken = '' + try { + const userInfoData = JSON.parse(decodeURIComponent(userInfoCookie.value)) + csrfToken = userInfoData.csrftoken || '' + } catch (e) { } + + if (!csrfToken) { + const csrfCookie = await chrome.cookies.get({ url: 'https://developer.huawei.com', name: 'csrfToken' }) + csrfToken = csrfCookie?.value || '' + } + + if (!csrfToken) return { loggedIn: true, username: '', avatar: '' } + + const response = await fetch('https://svc-drcn.developer.huawei.com/codeserver/Common/v1/delegate', { + method: 'POST', + credentials: 'include', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json;charset=UTF-8', + 'x-hd-csrf': csrfToken, + }, + body: JSON.stringify({ svc: 'GOpen.User.getInfo', reqType: 0, reqJson: JSON.stringify({ getNickName: '1' }) }) + }) + + const data = await response.json() + if (data.returnCode === '0' && data.resJson) { + const userInfo = JSON.parse(data.resJson) + return { + loggedIn: true, + username: userInfo.displayName || userInfo.loginID || '', + avatar: userInfo.headPictureURL || '' + } + } else { + return { loggedIn: true, username: '', avatar: '' } + } + } catch (e) { + return { loggedIn: false } + } +} + +export async function detectSspaiUser() { + try { + const jwtCookie = await chrome.cookies.get({ url: 'https://sspai.com', name: 'sspai_jwt_token' }) + if (!jwtCookie || !jwtCookie.value) return { loggedIn: false } + + const token = jwtCookie.value + const response = await fetch('https://sspai.com/api/v1/user/info/get', { + method: 'GET', + credentials: 'include', + headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${token}` } + }) + const data = await response.json() + + if (data.error === 0 && data.data?.nickname) { + return { loggedIn: true, username: data.data.nickname, avatar: data.data.avatar || '' } + } else { + return { loggedIn: false } + } + } catch (e) { return { loggedIn: false } } +} + +export async function detectAliyunUser() { + try { + const ticketCookie = await chrome.cookies.get({ url: 'https://developer.aliyun.com', name: 'login_aliyunid_ticket' }) + if (!ticketCookie || !ticketCookie.value) return { loggedIn: false } + + const response = await fetch('https://developer.aliyun.com/developer/api/my/user/getUser', { + method: 'GET', + credentials: 'include', + headers: { 'Accept': 'application/json' } + }) + const data = await response.json() + + if (data.success && data.data?.nickname) { + return { loggedIn: true, username: data.data.nickname, avatar: data.data.avatar || '' } + } + return { loggedIn: false } + } catch (e) { return { loggedIn: false } } +} + +export async function detectSohuUser() { + try { + const ppinfCookie = await chrome.cookies.get({ url: 'https://mp.sohu.com', name: 'ppinf' }) + if (!ppinfCookie || !ppinfCookie.value) return { loggedIn: false } + + try { + const response = await fetch('https://mp.sohu.com/mpbp/bp/account/list', { + method: 'GET', + credentials: 'include', + headers: { 'Accept': 'application/json' } + }) + const data = await response.json() + if (data.success && data.data?.data?.[0]?.accounts?.[0]) { + const account = data.data.data[0].accounts[0] + let avatar = account.avatar || '' + if (avatar.startsWith('//')) avatar = 'https:' + avatar + return { loggedIn: true, username: account.nickName, avatar } + } else { + return { loggedIn: true, username: '', avatar: '' } + } + } catch (e) { + return { loggedIn: true, username: '', avatar: '' } + } + } catch (e) { return { loggedIn: false } } +} + +export async function detectMediumUser() { + try { + const sidCookie = await chrome.cookies.get({ url: 'https://medium.com', name: 'sid' }) + const uidCookie = await chrome.cookies.get({ url: 'https://medium.com', name: 'uid' }) + + if (!sidCookie && !uidCookie) return { loggedIn: false } + + const response = await fetch('https://medium.com/me/stats', { + method: 'GET', + credentials: 'include', + }) + const html = await response.text() + const finalUrl = response.url + + if (finalUrl.includes('/m/signin') || finalUrl.includes('?signIn')) return { loggedIn: false } + + const profileMatch = html.match(/"username"\s*:\s*"([^"]+)"/) || + html.match(/href="https:\/\/medium\.com\/@([^"?\/]+)"/) || + html.match(/medium\.com\/@([a-zA-Z0-9_]+)/) + + if (profileMatch && profileMatch[1] && profileMatch[1] !== 'gmail' && profileMatch[1] !== 'medium') { + const username = profileMatch[1] + const shortName = username.replace(/\d+$/, '') + const avatarPattern = new RegExp(`<img[^>]*alt="${shortName}"[^>]*src="([^"]+)"`) + const avatarMatch = html.match(avatarPattern) || html.match(new RegExp(`<img[^>]*src="([^"]+)"[^>]*alt="${shortName}"`)) + + return { loggedIn: true, username, avatar: avatarMatch ? avatarMatch[1] : '' } + } else { + return { loggedIn: true, username: '', avatar: '' } + } + } catch (e) { return { loggedIn: false } } +} + +export async function detectTencentCloudUser() { + try { + const response = await fetch('https://cloud.tencent.com/developer/creator', { + method: 'GET', + credentials: 'include', + }) + const html = await response.text() + const finalUrl = response.url + + if (!finalUrl.includes('/creator')) return { loggedIn: false } + if (html.includes('登录/注册') || html.includes('"isLogin":false') || html.includes('"login":false')) return { loggedIn: false } + + const userInfoMatch = html.match(/"userInfo"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) || + html.match(/"creatorInfo"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) || + html.match(/"currentUser"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) + + const creatorNicknameMatch = html.match(/class="creator-info[^"]*"[^>]*>[\s\S]*?<[^>]*class="[^"]*name[^"]*"[^>]*>([^<]+)</) || + html.match(/"isCreator"\s*:\s*true[\s\S]*?"nickname"\s*:\s*"([^"]+)"/) + + const nicknameMatch = userInfoMatch || creatorNicknameMatch + const avatarMatch = html.match(/"userInfo"[\s\S]*?"avatarUrl"\s*:\s*"([^"]+)"/) || + html.match(/"avatar"\s*:\s*"(https?:\/\/[^"]+)"/) + + if (nicknameMatch && nicknameMatch[1]) { + return { loggedIn: true, username: nicknameMatch[1], avatar: avatarMatch ? avatarMatch[1] : '' } + } else { + if (html.includes('创作中心') || html.includes('我的文章')) return { loggedIn: true, username: '', avatar: '' } + return { loggedIn: false } + } + } catch (e) { return { loggedIn: false } } +} + +export async function detectQianfanUser() { + try { + const response = await fetch('https://qianfan.cloud.baidu.com/api/community/user/current', { + method: 'GET', + credentials: 'include', + headers: { 'Accept': 'application/json' } + }) + if (!response.ok) return { loggedIn: false } + + const data = await response.json() + if (data.success && data.result) { + const username = data.result.displayName || data.result.nickname || '' + const avatar = data.result.avatar || '' + return { loggedIn: true, username, avatar } + } else { + return { loggedIn: false } + } + } catch (e) { return { loggedIn: false } } +} + +export async function detectTwitterUser() { + try { + const authTokenCookie = await chrome.cookies.get({ url: 'https://x.com', name: 'auth_token' }) + const ct0Cookie = await chrome.cookies.get({ url: 'https://x.com', name: 'ct0' }) + + if (!authTokenCookie) return { loggedIn: false } + + let username = '' + let avatar = '' + + try { + const response = await fetch('https://x.com/home', { + method: 'GET', + credentials: 'include', + headers: { 'Accept': 'text/html' } + }) + if (response.ok) { + const html = await response.text() + const screenNameMatch = html.match(/"screen_name"\s*:\s*"([^"]+)"/) + if (screenNameMatch) username = screenNameMatch[1] + const avatarMatch = html.match(/"profile_image_url_https"\s*:\s*"([^"]+)"/) + if (avatarMatch) avatar = avatarMatch[1].replace('_normal.', '_x96.') + } + } catch (e) { } + + // If fetch failed or regex failed, try explicit fallback scraping logic (simplified here) + // Note: Full scrape logic from background.js is complex. + // We will assume basic fetch works or just return loggedIn:true if cookie exists + + return { loggedIn: true, username, avatar } + } catch (e) { return { loggedIn: false } } +} diff --git a/packages/detection/src/platforms/csdn.js b/packages/detection/src/platforms/csdn.js new file mode 100644 index 0000000..73deec3 --- /dev/null +++ b/packages/detection/src/platforms/csdn.js @@ -0,0 +1,53 @@ +/** + * CSDN platform detection logic + * Strategy: + * 1. Check 'UserName' cookie (reliable indicator of login) + * 2. Use 'UserNick' cookie for display name (UserName is the user ID, UserNick is the display name) + * 3. If logged in, fetch public blog page to get avatar + */ +export async function detectCSDNUser() { + try { + console.log('[COSE] CSDN Detection: Starting cookie check') + const userNameCookie = await chrome.cookies.get({ url: 'https://www.csdn.net', name: 'UserName' }) + + if (userNameCookie && userNameCookie.value) { + const userId = userNameCookie.value + console.log(`[COSE] CSDN UserName cookie found: ${userId}`) + + // UserNick cookie contains the display name (e.g. 'timerring') + const userNickCookie = await chrome.cookies.get({ url: 'https://www.csdn.net', name: 'UserNick' }) + const username = (userNickCookie && userNickCookie.value) ? decodeURIComponent(userNickCookie.value) : userId + console.log(`[COSE] CSDN display name: ${username}`) + + let avatar = '' + try { + // Fetch public blog page for avatar + const blogUrl = `https://blog.csdn.net/${userId}` + const blogResp = await fetch(blogUrl, { method: 'GET' }) + const blogHtml = await blogResp.text() + + // Extract avatar + const avatarMatch = blogHtml.match(/<img[^>]*src=["'](https:\/\/(?:profile|i-avatar)\.csdnimg\.cn\/[^"']+)["']/i) || + blogHtml.match(/<img[^>]*class=["']avatar[^"']*["'][^>]*src=["']([^"']+)["']/i) + + if (avatarMatch) { + avatar = avatarMatch[1] + } + } catch (e) { + console.warn('[COSE] CSDN Avatar fetch failed:', e) + } + + return { + loggedIn: true, + username: username, + avatar: avatar + } + } + + console.log('[COSE] CSDN: No login detected') + return { loggedIn: false } + } catch (e) { + console.error('[COSE] CSDN Detection Error:', e) + return { loggedIn: false, error: e.message } + } +} diff --git a/packages/detection/src/platforms/oschina.js b/packages/detection/src/platforms/oschina.js new file mode 100644 index 0000000..e1fced9 --- /dev/null +++ b/packages/detection/src/platforms/oschina.js @@ -0,0 +1,75 @@ +/** + * OSChina platform detection logic + * Strategy: + * 1. Fetch homepage, extract user ID or username + * 2. If only ID found, fetch personal space to get username + */ +export async function detectOSChinaUser() { + try { + console.log('[COSE] OSChina Detection: Starting') + const response = await fetch('https://www.oschina.net/', { + method: 'GET', + credentials: 'include' + }) + const html = await response.text() + + // Extract User ID + const uidMatch = html.match(/href=["']https:\/\/my\.oschina\.net\/u\/(\d+)["']/i) || + html.match(/space\.oschina\.net\/u\/(\d+)/i) || + html.match(/data-user-id=["'](\d+)["']/i) + + const userId = uidMatch ? uidMatch[1] : null + + // Extract Username + let username = '' + const nameMatch = html.match(/<a[^>]*class="[^"]*user-name[^"]*"[^>]*>([^<]+)<\/a>/i) || + html.match(/<span[^>]*class="[^"]*nick[^"]*"[^>]*>([^<]+)<\/span>/i) || + html.match(/title="([^"]+)"[^>]*class="[^"]*avatar/i) + + if (nameMatch) { + username = nameMatch[1].trim() + } + + // Fallback: Fetch personal space if only ID is known + if (!username && userId) { + console.log(`[COSE] OSChina: Fetching personal space for userId ${userId}`) + try { + const userPageResp = await fetch(`https://my.oschina.net/u/${userId}`, { + method: 'GET', + credentials: 'include' + }) + const userPageHtml = await userPageResp.text() + const userPageNameMatch = userPageHtml.match(/<h3[^>]*class="[^"]*header[^"]*"[^>]*>([^<]+)<\/h3>/i) || + userPageHtml.match(/<div[^>]*class="[^"]*user-name[^"]*"[^>]*>([^<]+)<\/div>/i) || + userPageHtml.match(/<title>([^<]+)的个人空间<\/title>/i) + if (userPageNameMatch) { + username = userPageNameMatch[1].trim() + } + } catch (e) { + console.warn('[COSE] OSChina: Failed to fetch user page', e) + } + } + + // Fallback: Use ID as username if name still missing + if (!username && userId) { + username = userId + } + + // Extract Avatar + let avatar = '' + const avatarMatch = html.match(/<img[^>]*src="([^"]+)"[^>]*class="[^"]*avatar/i) || + html.match(/<img[^>]*class="[^"]*avatar[^"]*"[^>]*src="([^"]+)"/i) + if (avatarMatch) { + avatar = avatarMatch[1] + } + + if (username) { + return { loggedIn: true, username, avatar } + } + + return { loggedIn: false } + } catch (e) { + console.error('[COSE] OSChina Detection Error:', e) + return { loggedIn: false, error: e.message } + } +} diff --git a/packages/detection/src/utils.js b/packages/detection/src/utils.js new file mode 100644 index 0000000..29beee4 --- /dev/null +++ b/packages/detection/src/utils.js @@ -0,0 +1,121 @@ +export async function logToStorage(msg, data = null) { + if (data) { + console.log(msg, data) + } else { + console.log(msg) + } +} + +// 通过 Cookie 检测登录状态 +export async function checkLoginByCookie(platformId, config) { + try { + // 直接按名称查找 cookie + const cookieMap = {} + if (config.cookieNames) { + for (const name of config.cookieNames) { + const cookie = await chrome.cookies.get({ + url: config.cookieUrl || `https://${config.cookieDomain}`, + name: name + }) + if (cookie) { + cookieMap[name] = cookie.value + } + } + } + + console.log(`[COSE] ${platformId} 找到的cookies:`, Object.keys(cookieMap)) + + const hasLoginCookie = config.cookieNames && config.cookieNames.some(name => cookieMap[name]) + + if (!hasLoginCookie) { + console.log(`[COSE] ${platformId} 未找到登录 cookie`) + return { loggedIn: false } + } + + // 自定义 cookie 值检测逻辑(如 InfoQ) + if (config.customCheck && config.checkCookieValue) { + console.log(`[COSE] ${platformId} 使用自定义 cookie 检测`) + const result = config.checkCookieValue(cookieMap) + console.log(`[COSE] ${platformId} 自定义检测结果:`, result) + return result + } + + let username = '' + let avatar = '' + + // 如果配置了从 cookie 获取用户名 + if (config.getUsernameFromCookie && config.usernameCookie) { + username = decodeURIComponent(cookieMap[config.usernameCookie] || '') + } + + + // 使用平台配置的 fetchAvatar 回调获取头像 + if (config.fetchAvatar && typeof config.fetchAvatar === 'function') { + try { + const fetchedAvatar = await config.fetchAvatar(cookieMap) + if (fetchedAvatar) { + avatar = fetchedAvatar + console.log(`[COSE] ${platformId} 找到头像:`, avatar) + } + } catch (e) { + console.log(`[COSE] ${platformId} 获取头像失败:`, e.message) + } + } + + return { loggedIn: true, username, avatar } + + } catch (e) { + console.log(`[COSE] ${platformId} Cookie 检测失败:`, e.message) + return { loggedIn: false, error: e.message } + } +} + +// 通用 API 检测逻辑 +export async function detectByApi(platformId, config) { + try { + console.log(`[COSE] ${platformId} 开始 API 检测: ${config.api}`) + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 8000) + + const fetchOptions = { + method: config.method || 'GET', + credentials: 'include', + headers: { + 'Accept': config.isHtml ? 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' : 'application/json', + 'Cache-Control': 'no-cache', + ...(config.headers || {}) + }, + signal: controller.signal, + } + + if (config.body) { + fetchOptions.body = config.body + } + + const response = await fetch(config.api, fetchOptions) + clearTimeout(timeoutId) + console.log(`[COSE] ${platformId} API 响应状态: ${response.status}`) + + let data = null + if (config.isHtml) { + // 明确指定为 HTML 响应时,使用 text() 解析 + try { data = await response.text() } catch (e) { data = '' } + } else { + // 其他情况尝试 JSON 解析 + try { data = await response.json() } catch (e) { data = null } + } + // console.log(`[COSE] ${platformId} API 数据:`, data) + + const loggedIn = config.checkLogin(data) + // console.log(`[COSE] ${platformId} checkLogin 结果: ${loggedIn}`) + if (loggedIn && config.getUserInfo) { + const userInfo = config.getUserInfo(data) + console.log(`[COSE] ${platformId} 用户信息:`, userInfo) + return { loggedIn: true, ...userInfo } + } + return { loggedIn: !!loggedIn } + } catch (error) { + console.log(`[COSE] ${platformId} API 检测失败: ${error.message}`) + return { loggedIn: false, error: error.message } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8feb707..1266a81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@cose/core': specifier: workspace:* version: link:../../packages/core + '@cose/detection': + specifier: workspace:* + version: link:../../packages/detection devDependencies: cac: specifier: ^6.7.14