BILIBILI_COOKIE环境变量配置界面增加扫码获取cookie功能 (#145)

* 更新 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
This commit is contained in:
Celestials316 authored and GitHub committed 2026-01-12 22:25:08 +08:00
1 parent ec1f92d767
commit 6b12112022
4 files changed
+598 -6

No files matched your search

+46
View File
@@ -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%;
}
}
`;
+217 -4
View File
@@ -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 = \`
<div class="bili-cookie-editor">
<div class="bili-cookie-status" id="bili-cookie-status">
<span class="bili-status-icon">🔍</span>
<span class="bili-status-text">检测中...</span>
</div>
<div class="bili-cookie-actions">
<button type="button" class="btn btn-primary btn-sm" onclick="startBilibiliQRLogin()">
📱 扫码登录
</button>
</div>
<label>Cookie 值</label>
<textarea class="form-group" id="text-value" placeholder="SESSDATA=xxx; bili_jct=xxx; DedeUserID=xxx;" rows="\${rows}">\${value}</textarea>
<div class="form-help">推荐使用扫码登录自动获取,或手动粘贴包含 SESSDATA 和 bili_jct 的完整 Cookie</div>
</div>
\`;
// 自动检测 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 = \`
<label>变量值 *</label>
<textarea id="text-value" placeholder="例如: localhost" rows="\${rows}" class="text-monospace">\${value}</textarea>
@@ -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 = \`
<div class="modal" id="bili-qr-modal">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h3>📱 扫码登录 Bilibili</h3>
<button class="close-btn" onclick="closeBiliQRModal()">×</button>
</div>
<div class="modal-body" style="text-align: center;">
<div id="bili-qr-container">
<div class="loading-spinner" id="bili-qr-loading"></div>
<p id="bili-qr-status">正在生成二维码...</p>
<div id="bili-qr-code" style="display: none;"></div>
</div>
</div>
</div>
</div>
\`;
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 = '<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=' + encodeURIComponent(qrUrl) + '" alt="二维码">';
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 = '<span class="bili-status-icon">⚠️</span><span class="bili-status-text">未配置</span>';
return;
}
statusEl.innerHTML = '<span class="bili-status-icon">🔍</span><span class="bili-status-text">检测中...</span>';
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 = \`<span class="bili-status-icon">✅</span><span class="bili-status-text">\${result.data.uname} (剩余 \${daysLeft} 天)</span>\`;
} else {
// 检测到无效时,提示用户保存并重新部署
showBilibiliCookieSaveHint();
}
} catch (error) {
statusEl.innerHTML = '<span class="bili-status-icon">⚠️</span><span class="bili-status-text">检测失败</span>';
}
}
// 显示 Bilibili Cookie 保存提示
function showBilibiliCookieSaveHint() {
const statusEl = document.getElementById('bili-cookie-status');
if (!statusEl) return;
statusEl.innerHTML = '<span class="bili-status-icon">💾</span><span class="bili-status-text">请点击保存按钮,Vercel等平台需重新部署后生效</span>';
}
`;
+303
View File
@@ -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);
}
}
+32 -2
View File
@@ -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);
}