From 6b1211202261ece5cde29fae888b7e69e032e0e1 Mon Sep 17 00:00:00 2001 From: Celestials316 Date: Mon, 12 Jan 2026 22:25:08 +0800 Subject: [PATCH] =?UTF-8?q?BILIBILI=5FCOOKIE=E7=8E=AF=E5=A2=83=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E9=85=8D=E7=BD=AE=E7=95=8C=E9=9D=A2=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=89=AB=E7=A0=81=E8=8E=B7=E5=8F=96cookie=E5=8A=9F=E8=83=BD=20?= =?UTF-8?q?(#145)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 更新 worker.js * 创建 cookie-util.js * 更新 systemsettings.js * 更新 components.css.js * 更新 cookie-util.js * 更新 cookie-util.js * 更新 cookie-util.js * 更新 cookie-util.js * 更新 worker.js * 更新 systemsettings.js * 更新 mango.js * 更新 mango.js * 更新 mango.js * 更新 mango.js * 更新 mango.js * 更新 mango.js * 更新 mango.js * 更新 mango.js * 更新 mango.js --- danmu_api/ui/css/components.css.js | 46 +++++ danmu_api/ui/js/systemsettings.js | 221 ++++++++++++++++++++- danmu_api/utils/cookie-util.js | 303 +++++++++++++++++++++++++++++ danmu_api/worker.js | 34 +++- 4 files changed, 598 insertions(+), 6 deletions(-) create mode 100644 danmu_api/utils/cookie-util.js diff --git a/danmu_api/ui/css/components.css.js b/danmu_api/ui/css/components.css.js index f6ec486..debf2a8 100644 --- a/danmu_api/ui/css/components.css.js +++ b/danmu_api/ui/css/components.css.js @@ -712,4 +712,50 @@ export const componentsCssContent = /* css */ ` display: inline-block; margin-left: 10px; } +/* Bilibili Cookie 编辑器样式 */ +.bili-cookie-editor { + display: flex; + flex-direction: column; + gap: 15px; +} + +.bili-cookie-status { + background: #f8f9fa; + padding: 12px; + border-radius: 6px; + display: flex; + align-items: center; + gap: 10px; + border-left: 4px solid #667eea; +} + +.bili-status-icon { + font-size: 18px; +} + +.bili-status-text { + flex: 1; + font-weight: 500; +} + +.bili-cookie-actions { + display: flex; + gap: 10px; +} + +.btn-sm { + padding: 6px 12px; + font-size: 13px; +} + +/* 移动端适配 */ +@media (max-width: 768px) { + .bili-cookie-actions { + flex-direction: column; + } + + .bili-cookie-actions .btn { + width: 100%; + } +} `; diff --git a/danmu_api/ui/js/systemsettings.js b/danmu_api/ui/js/systemsettings.js index 215c80a..4718c45 100644 --- a/danmu_api/ui/js/systemsettings.js +++ b/danmu_api/ui/js/systemsettings.js @@ -527,10 +527,37 @@ function renderValueInput(item) { } else { // 文本输入 - // 如果值太长,使用textarea而不是input - if (value && value.length > 50) { - // 计算行数,每行约50个字符 - const rows = Math.min(Math.max(Math.ceil(value.length / 50), 3), 10); // 最少3行,最多10行 + const currentKey = document.getElementById('env-key') ? document.getElementById('env-key').value : ''; + const isBilibiliCookie = currentKey === 'BILIBILI_COOKIE'; + + if (isBilibiliCookie) { + // Bilibili Cookie 专用编辑界面 + const rows = value && value.length > 50 ? Math.min(Math.max(Math.ceil(value.length / 50), 3), 8) : 3; + container.innerHTML = \` + + \`; + + // 自动检测 Cookie 状态 + setTimeout(() => { + autoCheckBilibiliCookieStatus(); + }, 100); + } else if (value && value.length > 50) { + const rows = Math.min(Math.max(Math.ceil(value.length / 50), 3), 10); container.innerHTML = \` @@ -1215,4 +1242,190 @@ function removeMapItem(button) { item.remove(); } } +/* ======================================== + Bilibili Cookie 扫码登录功能 + ======================================== */ +let biliQRCheckInterval = null; +let biliBiliQRKey = null; + +async function startBilibiliQRLogin() { + // 创建扫码登录模态框 + if (!document.getElementById('bili-qr-modal')) { + const modalHTML = \` + + \`; + document.body.insertAdjacentHTML('beforeend', modalHTML); + } + + const modal = document.getElementById('bili-qr-modal'); + const qrCode = document.getElementById('bili-qr-code'); + const qrLoading = document.getElementById('bili-qr-loading'); + const qrStatus = document.getElementById('bili-qr-status'); + + modal.classList.add('active'); + qrCode.style.display = 'none'; + qrCode.innerHTML = ''; + qrLoading.style.display = 'block'; + qrStatus.textContent = '正在生成二维码...'; + + if (biliQRCheckInterval) { + clearInterval(biliQRCheckInterval); + } + + try { + const response = await fetch(buildApiUrl('/api/cookie/qr/generate', true), { + method: 'POST' + }); + + const result = await response.json(); + + if (result.success && result.data) { + biliBiliQRKey = result.data.qrcode_key; + const qrUrl = result.data.url; + + qrCode.innerHTML = '二维码'; + qrCode.style.display = 'block'; + qrLoading.style.display = 'none'; + qrStatus.textContent = '请使用 Bilibili APP 扫描'; + + startBiliQRCheck(); + } else { + throw new Error(result.message || '生成二维码失败'); + } + } catch (error) { + qrLoading.style.display = 'none'; + qrStatus.textContent = '❌ ' + error.message; + } +} + +function startBiliQRCheck() { + if (!biliBiliQRKey) return; + + const qrStatus = document.getElementById('bili-qr-status'); + + biliQRCheckInterval = setInterval(async () => { + try { + const response = await fetch(buildApiUrl('/api/cookie/qr/check', true), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ qrcode_key: biliBiliQRKey }) + }); + + const result = await response.json(); + + if (result.success && result.data) { + const code = result.data.code; + + switch (code) { + case 86101: + qrStatus.textContent = '⏳ 等待扫码...'; + break; + case 86090: + qrStatus.textContent = '📱 已扫码,请确认'; + break; + case 86038: + qrStatus.textContent = '❌ 二维码已过期'; + clearInterval(biliQRCheckInterval); + break; + case 0: + qrStatus.textContent = '✅ 登录成功!'; + clearInterval(biliQRCheckInterval); + + if (result.data.cookie) { + fillBilibiliCookie(result.data.cookie); + } + + setTimeout(() => { + closeBiliQRModal(); + }, 1000); + break; + } + } + } catch (error) { + console.error('检查扫码状态失败:', error); + } + }, 2000); +} + +function fillBilibiliCookie(cookie) { + const textInput = document.getElementById('text-value'); + if (textInput) { + textInput.value = cookie; + textInput.dispatchEvent(new Event('input', { bubbles: true })); + + textInput.style.borderColor = 'var(--success-color, #28a745)'; + setTimeout(() => { + textInput.style.borderColor = ''; + // 填入后不立即检测,提示用户保存 + showBilibiliCookieSaveHint(); + }, 2000); + } +} + +function closeBiliQRModal() { + const modal = document.getElementById('bili-qr-modal'); + if (modal) { + modal.classList.remove('active'); + } + + if (biliQRCheckInterval) { + clearInterval(biliQRCheckInterval); + } +} + +async function autoCheckBilibiliCookieStatus() { + const textInput = document.getElementById('text-value'); + const statusEl = document.getElementById('bili-cookie-status'); + + if (!textInput || !statusEl) return; + + const cookie = textInput.value.trim(); + + // 如果输入框为空,提示未配置 + if (!cookie) { + statusEl.innerHTML = '⚠️未配置'; + return; + } + + statusEl.innerHTML = '🔍检测中...'; + + try { + const response = await fetch(buildApiUrl('/api/cookie/status', true)); + const result = await response.json(); + + if (result.success && result.data && result.data.isValid) { + const expiresAt = result.data.expiresAt; + const now = Math.floor(Date.now() / 1000); + const daysLeft = Math.ceil((expiresAt - now) / (24 * 60 * 60)); + + statusEl.innerHTML = \`✅\${result.data.uname} (剩余 \${daysLeft} 天)\`; + } else { + // 检测到无效时,提示用户保存并重新部署 + showBilibiliCookieSaveHint(); + } + } catch (error) { + statusEl.innerHTML = '⚠️检测失败'; + } +} +// 显示 Bilibili Cookie 保存提示 +function showBilibiliCookieSaveHint() { + const statusEl = document.getElementById('bili-cookie-status'); + if (!statusEl) return; + + statusEl.innerHTML = '💾请点击保存按钮,Vercel等平台需重新部署后生效'; +} `; diff --git a/danmu_api/utils/cookie-util.js b/danmu_api/utils/cookie-util.js new file mode 100644 index 0000000..c99d5fa --- /dev/null +++ b/danmu_api/utils/cookie-util.js @@ -0,0 +1,303 @@ +import { jsonResponse } from './http-util.js'; +import { log } from './log-util.js'; +import { Globals } from '../configs/globals.js'; +import { Envs } from '../configs/envs.js'; + +// 存储二维码登录会话(内存存储) +const qrLoginSessions = new Map(); + +/** + * 保存Cookie到环境变量 + */ +function saveCookieToGlobals(cookie) { + try { + if (Globals.envs) { + Globals.envs.bilibliCookie = cookie; + } + + if (Envs.env) { + Envs.env.BILIBILI_COOKIE = cookie; + } + + if (Envs.originalEnvVars && typeof Envs.originalEnvVars.set === 'function') { + Envs.originalEnvVars.set('BILIBILI_COOKIE', cookie); + } + + log("info", `Cookie已保存,长度: ${cookie ? cookie.length : 0}`); + } catch (err) { + log("error", `保存Cookie失败: ${err.message}`); + } +} + +/** + * 从多个位置获取Cookie + */ +function getCookieFromGlobals() { + if (Globals.envs && Globals.envs.bilibliCookie) { + return Globals.envs.bilibliCookie; + } + if (Envs.env && Envs.env.BILIBILI_COOKIE) { + return Envs.env.BILIBILI_COOKIE; + } + if (typeof process !== 'undefined' && process.env && process.env.BILIBILI_COOKIE) { + return process.env.BILIBILI_COOKIE; + } + return ''; +} + +/** + * 验证 Cookie 有效性 + */ +async function verifyCookieValidity(cookie) { + if (!cookie) { + return { isValid: false, error: '缺少Cookie' }; + } + + if (!cookie.includes('SESSDATA') || !cookie.includes('bili_jct')) { + return { isValid: false, error: 'Cookie格式不完整' }; + } + + try { + const response = await fetch('https://api.bilibili.com/x/web-interface/nav', { + method: 'GET', + headers: { + 'Cookie': cookie, + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + }); + + const data = await response.json(); + + if (data.code === 0 && data.data && data.data.isLogin) { + return { + isValid: true, + data: { + uname: data.data.uname, + mid: data.data.mid, + face: data.data.face + } + }; + } else { + return { isValid: false, error: data.message || 'Cookie无效或已过期' }; + } + } catch (error) { + return { isValid: false, error: '验证请求失败: ' + error.message }; + } +} + +/** + * 解析Cookie中的过期时间 + */ +function parseExpiresFromCookie(cookie) { + try { + const sessdataMatch = cookie.match(/SESSDATA=([^;]+)/); + if (sessdataMatch) { + let sessdata = sessdataMatch[1]; + try { + sessdata = decodeURIComponent(sessdata); + } catch (e) {} + + const parts = sessdata.split(','); + if (parts.length >= 2) { + const timestamp = parseInt(parts[1], 10); + const now = Math.floor(Date.now() / 1000); + if (!isNaN(timestamp) && timestamp > now) { + return timestamp; + } + } + } + } catch (e) {} + return Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60; // 默认30天 +} + +/** + * 获取Cookie状态 + */ +export async function handleCookieStatus() { + try { + const cookie = getCookieFromGlobals(); + + if (!cookie) { + return jsonResponse({ + success: true, + data: { isValid: false } + }); + } + + const verifyResult = await verifyCookieValidity(cookie); + + if (verifyResult.isValid) { + const expiresAt = parseExpiresFromCookie(cookie); + + return jsonResponse({ + success: true, + data: { + isValid: true, + uname: verifyResult.data.uname, + expiresAt: expiresAt + } + }); + } else { + return jsonResponse({ + success: true, + data: { + isValid: false, + error: verifyResult.error + } + }); + } + } catch (error) { + return jsonResponse({ success: false, message: error.message }, 500); + } +} + +/** + * 生成登录二维码 + */ +export async function handleQRGenerate() { + try { + const response = await fetch('https://passport.bilibili.com/x/passport-login/web/qrcode/generate', { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Referer': 'https://www.bilibili.com' + } + }); + + const data = await response.json(); + + if (data.code !== 0) { + return jsonResponse({ + success: false, + message: data.message || '生成失败' + }, 400); + } + + const qrcodeKey = data.data.qrcode_key; + const qrcodeUrl = data.data.url; + + qrLoginSessions.set(qrcodeKey, { + createTime: Date.now(), + status: 'pending' + }); + + const now = Date.now(); + for (const [key, session] of qrLoginSessions.entries()) { + if (now - session.createTime > 5 * 60 * 1000) { + qrLoginSessions.delete(key); + } + } + + return jsonResponse({ + success: true, + data: { + qrcode_key: qrcodeKey, + url: qrcodeUrl + } + }); + } catch (error) { + return jsonResponse({ + success: false, + message: error.message + }, 500); + } +} + +/** + * 检查二维码扫描状态 + */ +export async function handleQRCheck(request) { + try { + const body = await request.json(); + const qrcodeKey = body.qrcodeKey || body.qrcode_key; + + if (!qrcodeKey) { + return jsonResponse({ success: false, message: '缺少qrcodeKey参数' }, 400); + } + + const response = await fetch( + `https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=${qrcodeKey}`, + { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + } + ); + + const data = await response.json(); + let cookie = null; + + if (data.data.code === 0) { + // 从URL参数提取Cookie + if (data.data.url) { + const url = new URL(data.data.url); + const params = new URLSearchParams(url.search); + const SESSDATA = decodeURIComponent(params.get('SESSDATA') || ''); + const bili_jct = decodeURIComponent(params.get('bili_jct') || ''); + const DedeUserID = decodeURIComponent(params.get('DedeUserID') || ''); + + if (SESSDATA) { + cookie = `SESSDATA=${SESSDATA}; bili_jct=${bili_jct}; DedeUserID=${DedeUserID}`; + } + } + } + + const result = { + success: true, + data: { + code: data.data.code, + message: data.data.message || '' + } + }; + + if (cookie) { + result.data.cookie = cookie; + } + + return jsonResponse(result); + } catch (error) { + return jsonResponse({ success: false, message: error.message }, 500); + } +} + +/** + * 保存Cookie + */ +export async function handleCookieSave(request) { + try { + const body = await request.json(); + const cookie = body.cookie || ''; + + if (!cookie) { + return jsonResponse({ success: false, message: '缺少cookie参数' }, 400); + } + + if (!cookie.includes('SESSDATA') || !cookie.includes('bili_jct')) { + return jsonResponse({ + success: false, + message: 'Cookie格式不正确' + }, 400); + } + + const verifyResult = await verifyCookieValidity(cookie); + + if (!verifyResult.isValid) { + return jsonResponse({ + success: false, + message: 'Cookie验证失败: ' + (verifyResult.error || '无效') + }, 400); + } + + saveCookieToGlobals(cookie); + + return jsonResponse({ + success: true, + data: { + uname: verifyResult.data.uname + }, + message: 'Cookie保存成功' + }); + } catch (error) { + return jsonResponse({ success: false, message: error.message }, 500); + } +} \ No newline at end of file diff --git a/danmu_api/worker.js b/danmu_api/worker.js index a49d507..a5c0a10 100644 --- a/danmu_api/worker.js +++ b/danmu_api/worker.js @@ -8,6 +8,12 @@ import { getBangumi, getComment, getCommentByUrl, getSegmentComment, matchAnime, import { handleConfig, handleUI, handleLogs, handleClearLogs, handleDeploy, handleClearCache } from "./apis/system-api.js"; import { handleSetEnv, handleAddEnv, handleDelEnv } from "./apis/env-api.js"; import { Segment } from "./models/dandan-model.js" +import { + handleCookieStatus, + handleQRGenerate, + handleQRCheck, + handleCookieSave +} from "./utils/cookie-util.js"; let globals; @@ -115,7 +121,8 @@ async function handleRequest(req, env, deployPlatform, clientIp) { // 智能处理API路径前缀,确保最终有一个正确的 /api/v2 if (path !== "/" && path !== "/api/logs" && !path.startsWith('/api/env') - && !path.startsWith('/api/deploy') && !path.startsWith('/api/cache')) { + && !path.startsWith('/api/deploy') && !path.startsWith('/api/cache') + && !path.startsWith('/api/cookie') && !path.startsWith('/api/config')) { log("info", `[Path Check] Starting path normalization for: "${path}"`); const pathBeforeCleanup = path; // 保存清理前的路径检查是否修改 @@ -136,7 +143,8 @@ async function handleRequest(req, env, deployPlatform, clientIp) { // 2. 补全:如果路径缺少前缀(例如请求原始路径为 /search/anime),则补全 const pathBeforePrefixCheck = path; if (!path.startsWith('/api/v2') && path !== '/' && !path.startsWith('/api/logs') - && !path.startsWith('/api/env') && !path.startsWith('/api/env') && !path.startsWith('/api/cache')) { + && !path.startsWith('/api/env') && !path.startsWith('/api/cache') + && !path.startsWith('/api/cookie') && !path.startsWith('/api/config')) { log("info", `[Path Check] Path is missing /api/v2 prefix. Adding...`); path = '/api/v2' + path; } @@ -356,6 +364,28 @@ async function handleRequest(req, env, deployPlatform, clientIp) { return handleClearCache(); } + // ========== Cookie 管理 API ========== + + // GET /api/cookie/status - 获取Cookie状态 + if (path === "/api/cookie/status" && method === "GET") { + return handleCookieStatus(); + } + + // POST /api/cookie/qr/generate - 生成登录二维码 + if (path === "/api/cookie/qr/generate" && method === "POST") { + return handleQRGenerate(); + } + + // POST /api/cookie/qr/check - 检查二维码扫描状态 + if (path === "/api/cookie/qr/check" && method === "POST") { + return handleQRCheck(req); + } + + // POST /api/cookie/save - 保存Cookie + if (path === "/api/cookie/save" && method === "POST") { + return handleCookieSave(req); + } + return jsonResponse({ message: "Not found" }, 404); }